Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/specs/solid-monkeytype/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Serial; each step needs the previous one. Verify each in the browser (`__mt.chec
|---|---|---|---|---|---|
| [ ] | U1 | L · PR | Single-row user collection with `getUser()` and named writes (direct writes for server-side effects, optimistic actions for user changes, incl. quote ratings); convert **all writers** (result save, xp, badges, inbox count, lb memory, streak offset, quote rate, favourite quotes, edit profile, update name, unlink Discord, Discord OAuth, auth); delete the reactive mirror; `DB.getSnapshot()` becomes a read-only shim over the row | none | Writers must move in the same PR: there's no mutable object left |
| [ ] | U2 | M · PR | Activity calendar: plain activity data in the user row; the calendar becomes a Solid component; delete the imperative test-activity modules | U1 (data shape) | Can share a PR with U1 |
| [ ] | U3 | M · PR | Remove the snapshot-updated auth event (PSA, last-signed-out-result modal read `getUser()`); auth becomes a signal the router reads, with the user-data load as state (loading/loaded/failed) plus `reloadUserData()` replacing the event's promise; delete the auth bus | U1 | |
| [ ] | U3 | M · PR | Remove the snapshot-updated auth event (PSA, last-signed-out-result modal read `getUser()`); PSA's `authEvent` subscription (`Psa.init`, started by boot) follows the auth signal; auth becomes a signal the router reads, with the user-data load as state (loading/loaded/failed) plus `reloadUserData()` replacing the event's promise; delete the auth bus | U1 | |
| [ ] | U4 | S · commit per area | Readers move from the `DB.getSnapshot()` shim to `getUser()`, by area; the last PR deletes the shim and the snapshot code in `db.ts` | U1 | Parallel by area |
| [ ] | U5 | M · PR | Tags, presets and custom themes get the `getUser()` pattern: plain getters backed by a module-level reactive mirror, replacing their live-query hooks (keep `gcTime: Infinity`). Rewrite the non-test `__nonReactive` callers as Solid code reading the getters: commandline tag/preset lists, the custom-themes check in config metadata, the tag PB helpers in `db.ts` (or they go with U4) | none | Test-page callers go with T6a/T6c |
| [ ] | U6 | S · commit | Delete `__nonReactive` from every module once no caller is left | U5, T6a, T6c | Pure deletion: a caller still left means a step missed it |
Expand All @@ -130,10 +130,10 @@ Serial; each step needs the previous one. Verify each in the browser (`__mt.chec
|---|---|---|---|---|
| [x] | L1 | M · PR | Focus mode becomes a signal; layout binds focus state; cursor hiding goes to CSS; one component owns the mouse-move exit. Done in [#8519](https://github.com/monkeytypegame/monkeytype/pull/8519): `isFocusMode`/`setFocusMode` (rAF-deferred) in states/test; `data-focused` on app, header, footer, banners; `FocusModeExit`; caret blink follows the signal; `test/focus.ts` deleted | none (before T5) |
| [x] | L2 | L · PR | Funbox component owns funbox document state (body state, stylesheet link, CRT scanline); funbox functions stop touching the DOM and become engine plugin hooks, getting input as arguments instead of importing the EventLog; display tweaks via `data-ui-*` / funbox CSS. Done in [#8522](https://github.com/monkeytypegame/monkeytype/pull/8522): `components/core/Funbox.tsx` in App derives `fb-*`/`ignore-reduced-motion` and per-funbox links from `getAppliedFunboxes()` (set by `activate()` after validation, cleared by `clear()`), crt (`crtmode`, `#scanline`, global link) from `config.funbox` unless screenshotting; selectors unchanged; body classes via the `useBodyClasses` hook (Theme's `darkMode` and DevOptionsModal's debug classes moved too). `applyGlobalCSS`/`clearGlobal` and funbox's config-event subscriber deleted; crt loses `hasCssFile` (was loaded twice). Safari ≤16.5 crt check → `getFunboxUnsupportedReason` (metadata `isBlocked`, `toggleFunbox` setter, `activate()`). `handleKeydown`/`handleSpace` get a `FunboxInput` (`current()`, `forWord(i)`) from input instead of importing `events/data`. Hook shape left for T6a | none (before T5) |
| [ ] | L3 | M · PR | Monkey power as a component owning its canvas, positioned from the caret | T3 |
| [ ] | L3 | M · PR | Monkey power as a component owning its canvas, positioned from the caret; replaces App's `MonkeyPower.init` on mount (L6 stopgap) | T3 |
| [ ] | L4 | M · PR | Result word highlight becomes part of words history, driven by a highlighted-range signal from the chart | none |
| [ ] | L5 | M · PR | Screenshot-mode signal; elements hide through state; capture uses the result ref | none |
| [ ] | L6 | M · PR | Boot cleanup: the ready module goes into App lifecycle / a boot module; PSA, merch banner and no-CSS detection become state plus components | none |
| [ ] | L5 | M · PR | Screenshot-mode signal; elements hide through state (incl. the static `#nocss` box); capture uses the result ref | none |
| [x] | L6 | M · PR | Boot cleanup: the ready module goes into App lifecycle / a boot module; PSA, merch banner and no-CSS detection become state plus components. Done: DOM-free `boot.ts` (called after render) awaits config then auth, sets app ready, shows merch banner, syncs server config, registers the service worker, and starts connection listeners (`initConnection`, was `onDOMReady`) and PSA (`Psa.init`, still on the auth bus until U3); App calls `MonkeyPower.init` on mount until L3 (the body transition gate went in 99ef85bb6: the first paint has the stored theme); `NoCssDetails` portals into empty mount points of the static `#nocss` box; `ready.ts` and `no-css.ts` deleted; the app has no `onDOMReady` callers left (`email-handler.html` still uses it, see CL1) | none |
| [ ] | L7 | S · commit | Remove stray DOM queries: account page, leaderboard table, captcha, challenge controller header, ad/analytics controllers. Done except the leaderboard table: #8524 rewrites its scroll-to-user and should switch to DataTable's `rowSelection.scrollToActiveRow` | none |
| [ ] | L8 | S · commit | Delete dead modules: result batches, character counter, discord avatar util, input indicator, unused input-validation exports, tag builder plus its spec | none |
| [ ] | L9 | M · PR | Replace the sorted-table utility with DataTable | none |
Expand All @@ -145,7 +145,7 @@ Serial; each step needs the previous one. Verify each in the browser (`__mt.chec

| Done | ID | Size | Step | Blocked by |
|---|---|---|---|---|
| [ ] | CL1 | M · PR | Delete the DOM-wrapper utility library, the ref-with-utils hook and `onDOMReady` | T7, all L, U2 |
| [ ] | CL1 | M · PR | Delete the DOM-wrapper utility library, the ref-with-utils hook and `onDOMReady` (the app has none since L6; `email-handler.html`, a separate Vite entry, still imports `qs` and `onDOMReady` from `utils/dom`, so migrate that page first) | T7, all L, U2 |
| [ ] | CL2 | M · PR | DOM-query / `innerHTML` lint ban, with the server-HTML allowlist (notifications, simple modal, version history, quote-search highlight) | CL1 |
| [ ] | CL3 | M · PR | **Announced removal of legacy theme selectors** (old ids, classes, `letter`): the single break for user custom CSS; delete remaining SCSS | T1, L11, L12 |
| [ ] | CL4 | S · commit | Update coding standards and agent docs to describe only the final patterns | CL1 |
Expand Down
51 changes: 51 additions & 0 deletions frontend/__tests__/components/core/NoCssDetails.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { render } from "@solidjs/testing-library";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";

import { NoCssDetails } from "../../../src/ts/components/core/NoCssDetails";

function text(id: string): string {
return (document.getElementById(id)?.textContent ?? "")
.replace(/\s+/g, " ")
.trim();
}

describe("NoCssDetails", () => {
beforeEach(() => {
document.body.innerHTML = `
<div id="nocss">
<span id="nocss-keys"></span>
<span id="nocss-stylesheets"></span>
<span id="nocss-js"></span>
</div>`;
document.head.innerHTML = `<link rel="stylesheet" href="/css/style.abc.css"><link rel="stylesheet" href="/other.css">`;
});

afterEach(() => {
vi.unstubAllGlobals();
document.head.innerHTML = "";
});

it("shows Ctrl outside mac", () => {
vi.stubGlobal("navigator", { userAgent: "Mozilla/5.0 (X11; Linux)" });
render(() => <NoCssDetails />);

expect(text("nocss-keys")).toBe("Ctrl + Shift + R");
});

it("shows Cmd on mac", () => {
vi.stubGlobal("navigator", { userAgent: "Mozilla/5.0 (Macintosh)" });
render(() => <NoCssDetails />);

expect(text("nocss-keys")).toBe("Cmd + Shift + R");
});

it("lists only the app stylesheets and the client version", () => {
render(() => <NoCssDetails />);

expect(text("nocss-stylesheets")).toBe(
`Requested stylesheets:${location.origin}/css/style.abc.css`,
);
expect(text("nocss-js")).toMatch(/^Requested Javascript files:/);
expect(text("nocss-js")).toMatch(/Client version:\S+$/);
});
});
48 changes: 48 additions & 0 deletions frontend/__tests__/root/boot.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, it, expect, vi } from "vitest";

const { config, auth } = vi.hoisted(() => ({
config: Promise.withResolvers<undefined>(),
auth: Promise.withResolvers<undefined>(),
}));

vi.mock("../../src/ts/config/lifecycle", () => ({
configLoadPromise: config.promise,
}));
vi.mock("../../src/ts/firebase", () => ({ authPromise: auth.promise }));
vi.mock("../../src/ts/elements/merch-banner", () => ({
showIfNotClosedBefore: vi.fn(),
}));
vi.mock("../../src/ts/elements/psa", () => ({ init: vi.fn() }));
vi.mock("../../src/ts/states/connection", () => ({ initConnection: vi.fn() }));
vi.mock("../../src/ts/ape/server-configuration", () => ({
sync: vi.fn(async () => undefined),
}));
vi.mock("../../src/ts/utils/env", () => ({ isDevEnvironment: () => false }));

import * as ServerConfiguration from "../../src/ts/ape/server-configuration";
import { boot } from "../../src/ts/boot";
import * as MerchBanner from "../../src/ts/elements/merch-banner";
import * as Psa from "../../src/ts/elements/psa";
import { initConnection } from "../../src/ts/states/connection";
import { isAppReady } from "../../src/ts/states/core";

describe("boot", () => {
it("waits for config, then auth, before the app is ready", async () => {
const booted = boot();

// started before awaiting, so the first auth state change shows PSAs
expect(initConnection).toHaveBeenCalledOnce();
expect(Psa.init).toHaveBeenCalledOnce();

auth.resolve(undefined);
await Promise.resolve();
expect(isAppReady()).toBe(false);

config.resolve(undefined);
await booted;

expect(isAppReady()).toBe(true);
expect(MerchBanner.showIfNotClosedBefore).toHaveBeenCalledOnce();
expect(ServerConfiguration.sync).toHaveBeenCalledOnce();
});
});
56 changes: 3 additions & 53 deletions frontend/src/html/warnings.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,53 +108,7 @@
It seems like the CSS failed to load. Please try clearing your cache to
redownload the styles:
<br />
<span class="keys">
<span
style="
padding: 1rem;
display: inline-block;
border-radius: 1rem;
background: #2c2e31;
margin-top: 1rem;
margin-bottom: 1rem;
"
>
Ctrl
</span>
/
<span
style="
padding: 1rem;
display: inline-block;
border-radius: 1rem;
background: #2c2e31;
"
>
Cmd
</span>
+
<span
style="
padding: 1rem;
display: inline-block;
border-radius: 1rem;
background: #2c2e31;
"
>
Shift
</span>
+
<span
style="
padding: 1rem;
display: inline-block;
border-radius: 1rem;
background: #2c2e31;
"
>
R
</span>
</span>
<span class="keys" id="nocss-keys"></span>
<br />
You could also try to reinstall the service worker:
<br />
Expand All @@ -172,11 +126,7 @@
If that doesn't help contact support (support@monkeytype.com or
discord.gg/monkeytype)
</span>
<span class="requestedStylesheets">
If the website works for a bit but then this screen comes back, clear your
cache again and then on Monkeytype open the command line (esc) and search
for "Clear SW cache".
</span>
<span class="requestedJs"></span>
<span class="requestedStylesheets" id="nocss-stylesheets"></span>
<span class="requestedJs" id="nocss-js"></span>
</div>
</div>
21 changes: 14 additions & 7 deletions frontend/src/ts/ready.ts → frontend/src/ts/boot.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import * as MonkeyPower from "./elements/monkey-power";
import * as MerchBanner from "./elements/merch-banner";
import * as ServerConfiguration from "./ape/server-configuration";
import { configLoadPromise } from "./config/lifecycle";
import * as MerchBanner from "./elements/merch-banner";
import * as Psa from "./elements/psa";
import { authPromise } from "./firebase";
import { onDOMReady } from "./utils/dom";
import { isDevEnvironment } from "./utils/env";
import { initConnection } from "./states/connection";
import { setAppReady } from "./states/core";
import { isDevEnvironment } from "./utils/env";

/** App startup work that doesn't touch the DOM. Call once, right after render. */
export async function boot(): Promise<void> {
// before awaiting: PSAs show on the first auth state change
initConnection();
Psa.init();

onDOMReady(async () => {
await configLoadPromise;
await authPromise;

Expand All @@ -17,8 +22,10 @@ onDOMReady(async () => {

void ServerConfiguration.sync();

MonkeyPower.init();
registerServiceWorker();
}

function registerServiceWorker(): void {
if (isDevEnvironment()) {
void navigator.serviceWorker
.getRegistrations()
Expand All @@ -44,4 +51,4 @@ onDOMReady(async () => {
});
}
}
});
}
8 changes: 7 additions & 1 deletion frontend/src/ts/components/core/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
RouterContextProvider,
RouterProvider as RouteRenderer,
} from "@tanstack/solid-router";
import { JSXElement } from "solid-js";
import { JSXElement, onMount } from "solid-js";

import * as MonkeyPower from "../../elements/monkey-power";
import { queryClient } from "../../queries";
import { router } from "../../router/app-router";
import { getGlobalOffsetTop, isAppReady } from "../../states/core";
Expand All @@ -23,9 +24,13 @@ import { BarTimerProgress } from "../pages/test/live-stats/BarTimerProgress";
import { CustomBackground } from "./CustomBackground";
import { Font } from "./Font";
import { Funbox } from "./Funbox";
import { NoCssDetails } from "./NoCssDetails";
import { Theme } from "./Theme";

export function App(): JSXElement {
// until monkey power owns its canvas (L3)
onMount(() => MonkeyPower.init());

return (
<Providers>
<Overlays />
Expand Down Expand Up @@ -66,6 +71,7 @@ export function App(): JSXElement {
<Advertisement id="ad-footer" visible={"sellout"} />
</Anime>
<DevTools />
<NoCssDetails />
</Providers>
);
}
Expand Down
93 changes: 93 additions & 0 deletions frontend/src/ts/components/core/NoCssDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { For, JSXElement, Show } from "solid-js";
import { Portal } from "solid-js/web";
import { envConfig } from "virtual:env-config";

const keyStyle = {
padding: "1rem",
display: "inline-block",
"border-radius": "1rem",
background: "#2c2e31",
};

/**
* Fills the static no-CSS warning (warnings.html) with the reload shortcut for
* this platform and the requested stylesheets and scripts. Inline styles only:
* it shows when the CSS failed to load.
*/
export function NoCssDetails(): JSXElement {
const isMac = window.navigator.userAgent.toLowerCase().includes("mac");
const stylesheets = (
[...document.querySelectorAll("link[rel=stylesheet]")] as HTMLLinkElement[]
)
.map((l) => l.href)
.filter((l) => /\/css\/style/gi.test(l));
const scripts = (
[...document.querySelectorAll("script")] as HTMLScriptElement[]
)
.map((l) => l.src)
.filter((l) => /(\/js\/mon|\/js\/vendor)/gi.test(l));

return (
<>
<Mount at={document.getElementById("nocss-keys")}>
<span
style={{
...keyStyle,
"margin-top": isMac ? "1rem" : undefined,
"margin-bottom": "1rem",
}}
>
{isMac ? "Cmd" : "Ctrl"}
</span>{" "}
+ <span style={keyStyle}>Shift</span> + <span style={keyStyle}>R</span>
</Mount>
<Mount at={document.getElementById("nocss-stylesheets")}>
Requested stylesheets:
<For each={stylesheets}>
{(href) => (
<>
<br />
{href}
</>
)}
</For>
</Mount>
<Mount at={document.getElementById("nocss-js")}>
Requested Javascript files:
<For each={scripts}>
{(src) => (
<>
<br />
{src}
</>
)}
</For>
<br />
<br />
Client version:
<br />
{envConfig.clientVersion}
</Mount>
</>
);
}

/** Renders into an empty mount point of the static warning markup. */
function Mount(props: {
at: HTMLElement | null;
children: JSXElement;
}): JSXElement {
return (
<Show when={props.at}>
{(el) => (
<Portal
mount={el()}
// the wrapper div Portal adds must not break the inline text
ref={(wrapper) => (wrapper.style.display = "contents")}
>
{props.children}
</Portal>
)}
</Show>
);
}
Loading
Loading