diff --git a/.github/workflows/monkey-ci.yml b/.github/workflows/monkey-ci.yml index 7038729134ec..7c4f7684df9a 100644 --- a/.github/workflows/monkey-ci.yml +++ b/.github/workflows/monkey-ci.yml @@ -28,7 +28,7 @@ jobs: should-build-be: ${{ steps.export-changes.outputs.should-build-be }} should-build-fe: ${{ steps.export-changes.outputs.should-build-fe }} should-build-pkg: ${{ steps.export-changes.outputs.should-build-pkg }} - assets-json: ${{ steps.export-changes.outputs.assets-json }} + assets-or-styles: ${{ steps.export-changes.outputs.assets-or-styles }} steps: - name: Full checkout @@ -41,8 +41,9 @@ jobs: id: filter with: filters: | - json: + assets-or-styles: - 'frontend/static/**/*' + - '**/*.{scss,css}' be-src: - 'backend/**/*.{ts,js,json,lua,css,html}' - 'backend/package.json' @@ -64,13 +65,13 @@ jobs: echo "should-build-pkg=${{ steps.filter.outputs.pkg-src }}" >> $GITHUB_OUTPUT echo "should-build-be=${{ steps.filter.outputs.be-src }}" >> $GITHUB_OUTPUT echo "should-build-fe=${{ steps.filter.outputs.fe-src }}" >> $GITHUB_OUTPUT - echo "assets-json=${{ steps.filter.outputs.json }}" >> $GITHUB_OUTPUT + echo "assets-or-styles=${{ steps.filter.outputs.assets-or-styles }}" >> $GITHUB_OUTPUT prime-cache: name: prime-cache runs-on: ubuntu-latest needs: [pre-ci] - if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-json == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') + if: needs.pre-ci.outputs.should-build-be == 'true' || needs.pre-ci.outputs.should-build-fe == 'true' || needs.pre-ci.outputs.should-build-pkg == 'true' || needs.pre-ci.outputs.assets-or-styles == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') steps: - name: Checkout pnpm-lock uses: actions/checkout@v4 @@ -216,7 +217,7 @@ jobs: name: ci-assets needs: [pre-ci, prime-cache] runs-on: ubuntu-latest - if: needs.pre-ci.outputs.assets-json == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') + if: needs.pre-ci.outputs.assets-or-styles == 'true' || contains(github.event.pull_request.labels.*.name, 'force-full-ci') steps: - uses: actions/checkout@v4 with: @@ -228,6 +229,10 @@ jobs: id: filter with: filters: | + styles: + - '**/*.{scss,css}' + json: + - 'frontend/static/**/*.json' languages: - 'frontend/static/languages/**' quotes: @@ -265,7 +270,12 @@ jobs: - name: Install dependencies run: pnpm install + - name: Lint styles + if: steps.filter.outputs.styles == 'true' + run: npm run lint-styles + - name: Lint JSON + if: steps.filter.outputs.json == 'true' run: npm run lint-json-assets - name: Validate language assets diff --git a/.gitignore b/.gitignore index 2946d00bdf7b..e2a50cb343d3 100644 --- a/.gitignore +++ b/.gitignore @@ -132,4 +132,4 @@ frontend/static/webfonts-preview .turbo frontend/.env.sentry-build-plugin .claude/worktrees -1024MiB \ No newline at end of file +1024MiB diff --git a/README.md b/README.md index bdb5ad5c39aa..0e52fed331b0 100644 --- a/README.md +++ b/README.md @@ -69,4 +69,4 @@ All of the [contributors](https://github.com/monkeytypegame/monkeytype/graphs/co # Support -If you wish to support further development and feel extra awesome, you can [donate](https://ko-fi.com/monkeytype), [become a Patron](https://www.patreon.com/monkeytype) or [buy a t-shirt](https://www.monkeytype.store/). +If you wish to support further development and feel extra awesome, you can [donate](https://ko-fi.com/monkeytype), [become a Patron](https://www.patreon.com/monkeytype), or [buy a t-shirt](https://www.monkeytype.store/). diff --git a/backend/package.json b/backend/package.json index 0e3e6788ed5b..2706c897a8c1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -45,7 +45,7 @@ "mjml": "4.15.0", "mongodb": "6.3.0", "mustache": "4.2.0", - "nodemailer": "8.0.4", + "nodemailer": "8.0.5", "object-hash": "3.0.0", "prom-client": "15.1.3", "rate-limiter-flexible": "5.0.3", @@ -53,7 +53,7 @@ "string-similarity": "4.0.4", "swagger-stats": "0.99.7", "ua-parser-js": "0.7.33", - "uuid": "10.0.0", + "uuid": "14.0.0", "winston": "3.6.0", "zod": "3.23.8" }, @@ -77,7 +77,7 @@ "@types/swagger-stats": "0.95.11", "@types/ua-parser-js": "0.7.36", "@types/uuid": "10.0.0", - "@vitest/coverage-v8": "4.0.15", + "@vitest/coverage-v8": "4.1.5", "concurrently": "8.2.2", "openapi3-ts": "2.0.2", "oxlint": "1.60.0", diff --git a/backend/private/style.css b/backend/private/style.css index 1b274ef00684..72bc15ae7bf9 100644 --- a/backend/private/style.css +++ b/backend/private/style.css @@ -81,7 +81,7 @@ body { bottom: 3rem; background-color: var(--sub-alt-color); color: var(--text-color); - font-style: bold; + font-weight: bold; border-radius: 3px; padding: 1rem 2rem; cursor: pointer; @@ -189,10 +189,6 @@ input[type="checkbox"] { } } -.tooltip:hover .tooltip-text { - display: block; -} - .tooltip-text { display: none; color: var(--text-color); @@ -202,3 +198,7 @@ input[type="checkbox"] { padding: 10px; border-radius: var(--roundness); } + +.tooltip:hover .tooltip-text { + display: block; +} diff --git a/frontend/__tests__/components/common/AsyncContent.spec.tsx b/frontend/__tests__/components/common/AsyncContent.spec.tsx index 6902b670d586..39328af98d2a 100644 --- a/frontend/__tests__/components/common/AsyncContent.spec.tsx +++ b/frontend/__tests__/components/common/AsyncContent.spec.tsx @@ -142,7 +142,7 @@ describe("AsyncContent", () => { query: { result: string | Error; }, - options?: Omit, "query" | "queries" | "children">, + options?: Omit, "queries" | "children">, ): { container: HTMLElement; } { @@ -160,12 +160,18 @@ describe("AsyncContent", () => { })); return ( - )}> - {(data: string | undefined) => ( + )} + > + {({ resultData }) => ( <> static content - no data}> -
{data}
+ no data} + > +
{resultData()}
)} @@ -318,7 +324,10 @@ describe("AsyncContent", () => { first: string | Error | undefined; second: string | Error | undefined; }, - options?: Omit, "query" | "queries" | "children">, + options?: Omit< + Props<{ first: string; second: string }>, + "queries" | "children" + >, ): { container: HTMLElement; } { @@ -347,24 +356,20 @@ describe("AsyncContent", () => { })); type Q = { first: string | undefined; second: string | undefined }; + return ( )} > - {(results: { - first: string | undefined; - second: string | undefined; - }) => ( + {({ firstData, secondData }) => ( <> no data} > -
{results.first}
-
{results.second}
+
{firstData()}
+
{secondData()}
)} diff --git a/frontend/__tests__/components/common/Conditional.spec.tsx b/frontend/__tests__/components/common/Conditional.spec.tsx deleted file mode 100644 index 267cdf13dfc8..000000000000 --- a/frontend/__tests__/components/common/Conditional.spec.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { cleanup, render, screen } from "@solidjs/testing-library"; -import { createSignal } from "solid-js"; -import { afterEach, describe, expect, it } from "vitest"; - -import { Conditional } from "../../../src/ts/components/common/Conditional"; - -describe("Conditional", () => { - afterEach(() => { - cleanup(); - }); - - describe("static rendering", () => { - it("renders then when if is true", () => { - render(() => then content} />); - - expect(screen.getByText("then content")).toBeInTheDocument(); - }); - - it("renders then when if is a truthy object", () => { - render(() => ( - then content} /> - )); - - expect(screen.getByText("then content")).toBeInTheDocument(); - }); - - it("renders then when if is a truthy string", () => { - render(() => then content} />); - - expect(screen.getByText("then content")).toBeInTheDocument(); - }); - - it("renders else fallback when if is false", () => { - render(() => ( - then content} - else={
else content
} - /> - )); - - expect(screen.queryByText("then content")).not.toBeInTheDocument(); - expect(screen.getByText("else content")).toBeInTheDocument(); - }); - - it("renders else fallback when if is null", () => { - render(() => ( - then content} - else={
else content
} - /> - )); - - expect(screen.queryByText("then content")).not.toBeInTheDocument(); - expect(screen.getByText("else content")).toBeInTheDocument(); - }); - - it("renders else fallback when if is undefined", () => { - render(() => ( - then content} - else={
else content
} - /> - )); - - expect(screen.queryByText("then content")).not.toBeInTheDocument(); - expect(screen.getByText("else content")).toBeInTheDocument(); - }); - - it("renders else fallback when if is 0", () => { - render(() => ( - then content} - else={
else content
} - /> - )); - - expect(screen.queryByText("then content")).not.toBeInTheDocument(); - expect(screen.getByText("else content")).toBeInTheDocument(); - }); - - it("renders nothing when if is falsy and else is not provided", () => { - const { container } = render(() => ( - then content} /> - )); - - expect(screen.queryByText("then content")).not.toBeInTheDocument(); - expect(container.firstChild).toBeNull(); - }); - }); - - describe("then as function", () => { - it("passes the truthy value to then function", () => { - const obj: { label: string } | null = { label: "hello" }; - render(() => ( -
{value().label}
} /> - )); - - expect(screen.getByText("hello")).toBeInTheDocument(); - }); - - it("does not call then function when if is falsy", () => { - const obj: { label: string } | null = null; - render(() => ( - then content} - else={
else content
} - /> - )); - - expect(screen.queryByText("then content")).not.toBeInTheDocument(); - expect(screen.getByText("else content")).toBeInTheDocument(); - }); - }); - - describe("reactivity", () => { - it("switches from else to then when if becomes truthy", async () => { - const [condition, setCondition] = createSignal(false); - - render(() => ( - then content} - else={
else content
} - /> - )); - - expect(screen.queryByText("then content")).not.toBeInTheDocument(); - expect(screen.getByText("else content")).toBeInTheDocument(); - - setCondition(true); - - expect(screen.getByText("then content")).toBeInTheDocument(); - expect(screen.queryByText("else content")).not.toBeInTheDocument(); - }); - - it("switches from then to else when if becomes falsy", async () => { - const [condition, setCondition] = createSignal(true); - - render(() => ( - then content} - else={
else content
} - /> - )); - - expect(screen.getByText("then content")).toBeInTheDocument(); - - setCondition(false); - - expect(screen.queryByText("then content")).not.toBeInTheDocument(); - expect(screen.getByText("else content")).toBeInTheDocument(); - }); - - it("then JSXElement updates reactively when inner signal changes", async () => { - const [label, setLabel] = createSignal("initial"); - - render(() => {label()}} />); - - expect(screen.getByText("initial")).toBeInTheDocument(); - - setLabel("updated"); - - expect(screen.getByText("updated")).toBeInTheDocument(); - }); - - it("then JSXElement updates reactively when if changes from a signal", async () => { - const [data, setData] = createSignal(undefined); - - render(() => ( - {data()}} - else={
no data
} - /> - )); - - expect(screen.getByText("no data")).toBeInTheDocument(); - expect(screen.queryByTestId("content")).not.toBeInTheDocument(); - - setData("resolved"); - - expect(screen.getByTestId("content")).toHaveTextContent("resolved"); - expect(screen.queryByText("no data")).not.toBeInTheDocument(); - }); - - it("then function value accessor tracks reactive if", () => { - const [data, setData] = createSignal<{ name: string } | null>(null); - - render(() => ( -
{value().name}
} - else={
no data
} - /> - )); - - expect(screen.getByText("no data")).toBeInTheDocument(); - - setData({ name: "Alice" }); - - expect(screen.getByTestId("content")).toHaveTextContent("Alice"); - - setData({ name: "Bob" }); - - expect(screen.getByTestId("content")).toHaveTextContent("Bob"); - }); - }); -}); diff --git a/frontend/__tests__/components/common/anime/AnimeConditional.spec.tsx b/frontend/__tests__/components/common/anime/AnimeConditional.spec.tsx deleted file mode 100644 index a120611baebf..000000000000 --- a/frontend/__tests__/components/common/anime/AnimeConditional.spec.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { cleanup, render, screen } from "@solidjs/testing-library"; -import { createSignal } from "solid-js"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; - -const { mockAnimate } = vi.hoisted(() => ({ - mockAnimate: vi.fn().mockImplementation(() => ({ - pause: vi.fn(), - then: vi.fn((cb: () => void) => { - cb(); - return Promise.resolve(); - }), - })), -})); - -vi.mock("animejs", () => ({ - animate: mockAnimate, -})); - -vi.mock("../../../../src/ts/utils/misc", () => ({ - applyReducedMotion: vi.fn((duration: number) => duration), -})); - -import { AnimeConditional } from "../../../../src/ts/components/common/anime/AnimeConditional"; - -describe("AnimeConditional", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - cleanup(); - }); - - it("renders `then` content when `if` is truthy", () => { - render(() => ( - then} - else={
else
} - /> - )); - - expect(screen.getByTestId("then-content")).toBeInTheDocument(); - expect(screen.queryByTestId("else-content")).not.toBeInTheDocument(); - }); - - it("renders `else` content when `if` is falsy", () => { - render(() => ( - then} - else={
else
} - /> - )); - - expect(screen.queryByTestId("then-content")).not.toBeInTheDocument(); - expect(screen.getByTestId("else-content")).toBeInTheDocument(); - }); - - it("renders `else` content when `if` is null", () => { - render(() => ( - then} - else={
else
} - /> - )); - - expect(screen.queryByTestId("then-content")).not.toBeInTheDocument(); - expect(screen.getByTestId("else-content")).toBeInTheDocument(); - }); - - it("switches reactively from `then` to `else`", () => { - const [condition, setCondition] = createSignal(true); - - render(() => ( - then} - else={
else
} - /> - )); - - expect(screen.getByTestId("then-content")).toBeInTheDocument(); - - setCondition(false); - - expect(screen.queryByTestId("then-content")).not.toBeInTheDocument(); - expect(screen.getByTestId("else-content")).toBeInTheDocument(); - }); - - it("switches reactively from `else` to `then`", () => { - const [condition, setCondition] = createSignal(false); - - render(() => ( - then} - else={
else
} - /> - )); - - expect(screen.getByTestId("else-content")).toBeInTheDocument(); - - setCondition(true); - - expect(screen.getByTestId("then-content")).toBeInTheDocument(); - expect(screen.queryByTestId("else-content")).not.toBeInTheDocument(); - }); - - it("supports `then` as a function and passes the truthy value", () => { - const obj = { label: "hello" }; - render(() => ( -
{value().label}
} - /> - )); - - expect(screen.getByTestId("fn-content")).toHaveTextContent("hello"); - }); - - it("does not throw without `else` prop", () => { - expect(() => { - render(() => ( - then} - /> - )); - }).not.toThrow(); - - expect(screen.getByTestId("then-content")).toBeInTheDocument(); - }); - - it("does not throw on mount/unmount", () => { - const [show, setShow] = createSignal(true); - - expect(() => { - render(() => ( - then} - else={
else
} - /> - )); - }).not.toThrow(); - - expect(() => setShow(false)).not.toThrow(); - expect(() => setShow(true)).not.toThrow(); - }); - - describe("default animations (opacity fade)", () => { - it("applies default opacity animate on `then` branch", () => { - render(() => then} />); - - expect(mockAnimate).toHaveBeenCalledWith( - expect.any(HTMLElement), - expect.objectContaining({ opacity: 1, duration: 125 }), - ); - }); - - it("applies default opacity initial state on `then` branch", () => { - render(() => then} />); - - // Initial call: opacity:0 with duration:0 - expect(mockAnimate).toHaveBeenCalledWith( - expect.any(HTMLElement), - expect.objectContaining({ opacity: 0, duration: 0 }), - ); - }); - }); - - describe("custom animeProps", () => { - it("uses custom animate params when animeProps provided", () => { - render(() => ( - then} - animeProps={{ - initial: { opacity: 0, translateY: -10 }, - animate: { opacity: 1, translateY: 0, duration: 400 }, - exit: { opacity: 0, translateY: -10, duration: 200 }, - }} - /> - )); - - expect(mockAnimate).toHaveBeenCalledWith( - expect.any(HTMLElement), - expect.objectContaining({ opacity: 1, translateY: 0, duration: 400 }), - ); - }); - - it("uses custom initial state when animeProps provided", () => { - render(() => ( - then} - animeProps={{ - initial: { opacity: 0, translateY: -10 }, - animate: { opacity: 1, translateY: 0, duration: 400 }, - }} - /> - )); - - // Initial state applied with duration:0 - expect(mockAnimate).toHaveBeenCalledWith( - expect.any(HTMLElement), - expect.objectContaining({ opacity: 0, translateY: -10, duration: 0 }), - ); - }); - }); - - it("exitBeforeEnter prop does not throw on condition change", () => { - const [cond, setCond] = createSignal(true); - - expect(() => { - render(() => ( - then} - else={
else
} - /> - )); - }).not.toThrow(); - - expect(() => setCond(false)).not.toThrow(); - }); -}); diff --git a/frontend/package.json b/frontend/package.json index 0723a36d7a39..38af68dbf817 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,6 +6,8 @@ "scripts": { "lint": "oxlint . --type-aware --type-check", "lint-fast": "oxlint .", + "lint-styles": "stylelint \"**/*.{scss,css}\"", + "lint-styles-fix": "stylelint \"**/*.{scss,css}\" --fix", "lint-json": "eslint static/**/*.json", "check-assets": "tsx ./scripts/check-assets.ts", "audit": "vite-bundle-visualizer", @@ -74,7 +76,7 @@ "zod-urlsearchparams": "0.0.16" }, "devDependencies": { - "@eslint/json": "1.0.1", + "@eslint/json": "1.2.0", "@fortawesome/fontawesome-free": "5.15.4", "@monkeytype/oxlint-config": "workspace:*", "@monkeytype/typescript-config": "workspace:*", @@ -92,7 +94,7 @@ "@types/object-hash": "3.0.6", "@types/subset-font": "1.4.3", "@types/throttle-debounce": "5.0.2", - "@vitest/coverage-v8": "4.0.15", + "@vitest/coverage-v8": "4.1.5", "autoprefixer": "10.4.27", "caniuse-lite": "1.0.30001778", "concurrently": "8.2.2", diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html index de4b16820652..49f01e1ba8a9 100644 --- a/frontend/src/html/pages/test.html +++ b/frontend/src/html/pages/test.html @@ -1,5 +1,5 @@ ); } diff --git a/frontend/src/ts/components/common/LoadingCircle.tsx b/frontend/src/ts/components/common/LoadingCircle.tsx index 5d7ac78fca05..6feaddb8a38a 100644 --- a/frontend/src/ts/components/common/LoadingCircle.tsx +++ b/frontend/src/ts/components/common/LoadingCircle.tsx @@ -1,7 +1,6 @@ -import { JSXElement } from "solid-js"; +import { JSXElement, Show } from "solid-js"; import { cn } from "../../utils/cn"; -import { Conditional } from "./Conditional"; import { Fa } from "./Fa"; export function LoadingCircle(props: { @@ -10,24 +9,9 @@ export function LoadingCircle(props: { class?: string; }): JSXElement { return ( - - - - } - else={ + } - /> + > +
+ +
+
); } diff --git a/frontend/src/ts/components/common/Separator.tsx b/frontend/src/ts/components/common/Separator.tsx index 4e0594b83c9e..ddb4caa10bbf 100644 --- a/frontend/src/ts/components/common/Separator.tsx +++ b/frontend/src/ts/components/common/Separator.tsx @@ -1,7 +1,6 @@ -import { JSXElement } from "solid-js"; +import { JSXElement, Show } from "solid-js"; import { cn } from "../../utils/cn"; -import { Conditional } from "./Conditional"; export function Separator(props: { class?: string; @@ -9,40 +8,39 @@ export function Separator(props: { text?: string; }): JSXElement { return ( - -
-
{props.text}
-
- + > } - else={ + > +
- } - /> +
{props.text}
+
+
+
); } diff --git a/frontend/src/ts/components/common/User.tsx b/frontend/src/ts/components/common/User.tsx index d5b57acde2e1..ebb53d6c25af 100644 --- a/frontend/src/ts/components/common/User.tsx +++ b/frontend/src/ts/components/common/User.tsx @@ -8,9 +8,9 @@ import { UserFlagOptions, } from "../../controllers/user-flag-controller"; import { cn } from "../../utils/cn"; -import { Anime, AnimeConditional } from "./anime"; +import { Anime } from "./anime"; +import { AnimePresence } from "./anime/AnimePresence"; import { Button } from "./Button"; -import { Conditional } from "./Conditional"; import { DiscordAvatar } from "./DiscordAvatar"; import { Fa } from "./Fa"; import { NotificationBubble } from "./NotificationBubble"; @@ -85,23 +85,37 @@ export function User(props: Props): JSXElement { class="z-2 m-0.5" />
- } - else={ - - } - /> + + + + + } + > + + + + +
@@ -110,19 +124,15 @@ export function User(props: Props): JSXElement { "hidden sm:block": props.hideNameOnSmallScreens, })} > - - } - else={props.user.name} - /> + + - ( - <> -
{ - if (isCoarse()) { - if (e.target instanceof HTMLAnchorElement) { - if (e.target.dataset["navItem"] === "account") { - e.preventDefault(); - e.stopPropagation(); - } - setAccountMenuOpen((prev) => !prev); - } - } - }} + + + + + - - { - // if (isCoarse()) { - // setAccountMenuOpen(false); - // } - // }} - /> -
-
- -
- - )} - else={ - - } - /> + > + + ); diff --git a/frontend/src/ts/components/layout/overlays/Notifications.tsx b/frontend/src/ts/components/layout/overlays/Notifications.tsx index 425e6ee8518d..2ecc6f4a039c 100644 --- a/frontend/src/ts/components/layout/overlays/Notifications.tsx +++ b/frontend/src/ts/components/layout/overlays/Notifications.tsx @@ -1,5 +1,5 @@ import { AnimationParams } from "animejs"; -import { For, JSXElement } from "solid-js"; +import { For, JSXElement, Show } from "solid-js"; import { getGlobalOffsetTop, getIsScreenshotting } from "../../../states/core"; import { @@ -13,7 +13,6 @@ import { cn } from "../../../utils/cn"; import { Anime } from "../../common/anime/Anime"; import { AnimePresence } from "../../common/anime/AnimePresence"; import { AnimeShow } from "../../common/anime/AnimeShow"; -import { Conditional } from "../../common/Conditional"; import { Fa, FaProps } from "../../common/Fa"; const levelConfig = { @@ -97,12 +96,13 @@ function NotificationItem(props: { notification: Notification }): JSXElement { {title()} - } - else={
{props.notification.message}
} - /> + {props.notification.message}} + > + {/* oxlint-disable-next-line solid/no-innerhtml */} +
+
diff --git a/frontend/src/ts/components/modals/CustomGeneratorModal.tsx b/frontend/src/ts/components/modals/CustomGeneratorModal.tsx index 0c5b83362820..4d7631f8fb13 100644 --- a/frontend/src/ts/components/modals/CustomGeneratorModal.tsx +++ b/frontend/src/ts/components/modals/CustomGeneratorModal.tsx @@ -163,6 +163,7 @@ export function CustomGeneratorModal(props: {
(
( field().handleChange(val ?? "")} diff --git a/frontend/src/ts/components/modals/VersionHistoryModal.tsx b/frontend/src/ts/components/modals/VersionHistoryModal.tsx index 44764bd14717..c6da79425ff6 100644 --- a/frontend/src/ts/components/modals/VersionHistoryModal.tsx +++ b/frontend/src/ts/components/modals/VersionHistoryModal.tsx @@ -34,13 +34,13 @@ export function VersionHistoryModal(): JSXElement { onScroll={fetchMoreVersions} > - {(data) => ( + {({ releasesData }) => ( <>
- it.releases)}> + it.releases)}> {(release) => }
diff --git a/frontend/src/ts/components/modals/WordFilterModal.tsx b/frontend/src/ts/components/modals/WordFilterModal.tsx index 84bd072111cd..010a23071722 100644 --- a/frontend/src/ts/components/modals/WordFilterModal.tsx +++ b/frontend/src/ts/components/modals/WordFilterModal.tsx @@ -231,6 +231,7 @@ export function WordFilterModal(props: {
- {(data) => ( + {({ typingStatsData }) => (
typingStatsData()?.testsStarted, + ], + ["total typing time", () => typingStatsData()?.timeTyping], + [ + "total tests completed", + () => typingStatsData()?.testsCompleted, + ], ] as const } > - {([title, data]) => ( + {([title, stat]) => (
{title}
-
{data?.text ?? "-"}
-
{data?.subText ?? "-"}
+
{stat()?.text ?? "-"}
+
{stat()?.subText ?? "-"}
)}
@@ -93,20 +99,21 @@ export function AboutPage(): JSXElement {
- {(data) => ( + {({ speedHistogramData }) => ( <>
distribution of time 60 leaderboard results (wpm)
- {numberOfHistogramRecords(data?.data)} total results + {numberOfHistogramRecords(speedHistogramData()?.data)} total + results
)} @@ -403,17 +411,17 @@ export function AboutPage(): JSXElement { text="top supporters" /> - {(data) => ( + {({ supportersData }) => (
- {(name) =>
{name}
}
+ {(name) =>
{name}
}
)}
@@ -426,17 +434,17 @@ export function AboutPage(): JSXElement { text="contributors" /> - {(data) => ( + {({ contributorsData }) => (
- {(name) =>
{name}
}
+ {(name) =>
{name}
}
)}
diff --git a/frontend/src/ts/components/pages/leaderboard/LeaderboardPage.tsx b/frontend/src/ts/components/pages/leaderboard/LeaderboardPage.tsx index 15edb775c214..ffb05ddda3ec 100644 --- a/frontend/src/ts/components/pages/leaderboard/LeaderboardPage.tsx +++ b/frontend/src/ts/components/pages/leaderboard/LeaderboardPage.tsx @@ -66,7 +66,7 @@ export function LeaderboardPage(): JSXElement { //update url after the data is loaded createEffect(() => { - if (isOpen() && dataQuery.isSuccess) { + if (isOpen() && entriesQuery.isSuccess) { updateGetParameters(getSelection(), getPage()); } }); @@ -90,7 +90,7 @@ export function LeaderboardPage(): JSXElement { } }); - const dataQuery = useQuery(() => ({ + const entriesQuery = useQuery(() => ({ ...getLeaderboardQueryOptions({ ...getSelection(), page: getPage() ?? 0, @@ -170,13 +170,18 @@ export function LeaderboardPage(): JSXElement {
- - {(config) => ( + + {({ serverConfigurationQueryData }) => ( )} @@ -191,51 +196,61 @@ export function LeaderboardPage(): JSXElement { /> } > - {({ data, rank, config }) => ( - - )} + {({ + entriesQueryData, + rankQueryData, + serverConfigurationQueryData, + }) => { + const minWpm = () => { + const d = entriesQueryData(); + return d && "minWpm" in d ? (d.minWpm as number) : undefined; + }; + + return ( + + ); + }}
} > - {(data) => ( + {({ entriesQueryData }) => (
setScrollToUser(false)} @@ -270,7 +287,9 @@ export function LeaderboardPage(): JSXElement {
, - }} - /> - } - else={ + } - /> + > + , + }} + /> + ); } diff --git a/frontend/src/ts/components/pages/leaderboard/UserRank.tsx b/frontend/src/ts/components/pages/leaderboard/UserRank.tsx index 77ab276bea08..2143df9a5caa 100644 --- a/frontend/src/ts/components/pages/leaderboard/UserRank.tsx +++ b/frontend/src/ts/components/pages/leaderboard/UserRank.tsx @@ -7,7 +7,6 @@ import { createMemo, JSXElement, Match, Show, Switch } from "solid-js"; import { getConfig } from "../../../config/store"; import { Formatting } from "../../../utils/format"; -import { Conditional } from "../../common/Conditional"; import { Fa } from "../../common/Fa"; import { LoadingCircle } from "../../common/LoadingCircle"; import { Table, TableEntry } from "./Table"; @@ -75,18 +74,9 @@ export function UserRank(props: { when={props.data !== undefined && props.total !== undefined} fallback={} > - - } - else={ + @@ -120,7 +110,15 @@ export function UserRank(props: {
} - /> + > +
+ ); diff --git a/frontend/src/ts/components/pages/login/LoginPage.tsx b/frontend/src/ts/components/pages/login/LoginPage.tsx index 13c37430805f..6e3c5f248bf9 100644 --- a/frontend/src/ts/components/pages/login/LoginPage.tsx +++ b/frontend/src/ts/components/pages/login/LoginPage.tsx @@ -4,7 +4,6 @@ import { JSXElement, Show } from "solid-js"; import { getServerConfigurationQueryOptions } from "../../../queries/server-configuration"; import { getActivePage } from "../../../states/core"; import { getLoginPageInputsEnabled } from "../../../states/login"; -import { Conditional } from "../../common/Conditional"; import { Login } from "./Login"; import { Register } from "./Register"; @@ -22,22 +21,21 @@ export function LoginPage(): JSXElement { - -

- Login/Signup is disabled or the server is down/under maintenance. -

- - } - else={ + } - /> + > +
+

+ Login/Signup is disabled or the server is down/under maintenance. +

+
+
); } diff --git a/frontend/src/ts/components/pages/profile/ProfilePage.tsx b/frontend/src/ts/components/pages/profile/ProfilePage.tsx index 122bd205d302..a7197213f095 100644 --- a/frontend/src/ts/components/pages/profile/ProfilePage.tsx +++ b/frontend/src/ts/components/pages/profile/ProfilePage.tsx @@ -20,8 +20,10 @@ export function ProfilePage(): JSXElement { return (
- - {(profile) => } + + {({ profileQueryData }) => ( + + )}
diff --git a/frontend/src/ts/components/pages/profile/UserDetails.tsx b/frontend/src/ts/components/pages/profile/UserDetails.tsx index 6bf7746ea68d..1b2790556dac 100644 --- a/frontend/src/ts/components/pages/profile/UserDetails.tsx +++ b/frontend/src/ts/components/pages/profile/UserDetails.tsx @@ -16,10 +16,10 @@ import { createEffect, createSignal, For, JSXElement, Show } from "solid-js"; import { Snapshot } from "../../../constants/default-snapshot"; import { addFriend, isFriend } from "../../../db"; -import * as EditProfileModal from "../../../modals/edit-profile"; import * as UserReportModal from "../../../modals/user-report"; import { bp } from "../../../states/breakpoints"; import { getUserId, isAuthenticated } from "../../../states/core"; +import { showModal } from "../../../states/modals"; import { showNoticeNotification, showErrorNotification, @@ -33,10 +33,10 @@ import { AutoShrink } from "../../common/AutoShrink"; import { Balloon, BalloonProps } from "../../common/Balloon"; import { Bar } from "../../common/Bar"; import { Button } from "../../common/Button"; -import { Conditional } from "../../common/Conditional"; import { DiscordAvatar } from "../../common/DiscordAvatar"; import { UserBadge } from "../../common/UserBadge"; import { UserFlags } from "../../common/UserFlags"; +import { EditProfile } from "../../popups/EditProfile"; type Variant = "basic" | "hasSocials" | "hasBioOrKeyboard" | "full"; @@ -99,6 +99,9 @@ export function UserDetails(props: { isAccountPage={props.isAccountPage} />
+ + +
); } @@ -110,7 +113,6 @@ function ActionButtons(props: { const isUsersProfile = () => props.profile.uid !== undefined && props.profile.uid === (getUserId() ?? ""); - const [hasFriendRequest, setHasFriendRequest] = createSignal(false); const showFriendsButton = () => isAuthenticated() && !isUsersProfile() && !hasFriendRequest(); @@ -135,47 +137,9 @@ function ActionButtons(props: { }; return ( - - + + {(badge) => ( + + )} + + + )} + + + +
+ + + {(field) => ( + + )} + +
+ save + + + ); +} diff --git a/frontend/src/ts/components/popups/alerts/Inbox.tsx b/frontend/src/ts/components/popups/alerts/Inbox.tsx index 1783575f238b..ecdf629cdac6 100644 --- a/frontend/src/ts/components/popups/alerts/Inbox.tsx +++ b/frontend/src/ts/components/popups/alerts/Inbox.tsx @@ -117,12 +117,14 @@ export function Inbox(): JSXElement { } body={ } > - {(inbox) => ( + {({ inboxQueryData }) => ( <> - it.status === "unclaimed")}> + it.status === "unclaimed")} + >