From 67f7e6a85b600f461078813d7e2b18081763d9b4 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 11 Feb 2026 11:41:20 +0100 Subject: [PATCH 1/4] impr: use tanstack/query for VersionHistoryModal (@fehmer) --- .../components/modals/VersionHistoryModal.tsx | 45 +++------------- frontend/src/ts/queries/public.ts | 52 ++++++++++++++++++- frontend/src/ts/utils/json-data.ts | 2 +- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/frontend/src/ts/components/modals/VersionHistoryModal.tsx b/frontend/src/ts/components/modals/VersionHistoryModal.tsx index 85556adceb27..36e4087282f1 100644 --- a/frontend/src/ts/components/modals/VersionHistoryModal.tsx +++ b/frontend/src/ts/components/modals/VersionHistoryModal.tsx @@ -1,52 +1,23 @@ -import { format } from "date-fns/format"; -import { JSXElement, createResource, For } from "solid-js"; +import { useQuery } from "@tanstack/solid-query"; +import { For, JSXElement } from "solid-js"; +import { getVersionHistoryQueryOptions } from "../../queries/public"; import { isModalOpen } from "../../stores/modals"; -import { getReleasesFromGitHub } from "../../utils/json-data"; import { AnimatedModal } from "../common/AnimatedModal"; import AsyncContent from "../common/AsyncContent"; export function VersionHistoryModal(): JSXElement { const isOpen = (): boolean => isModalOpen("VersionHistory"); - const [releases] = createResource(isOpen, async (open) => { - if (!open) return null; - const releases = await getReleasesFromGitHub(); - const data = []; - for (const release of releases) { - if (release.draft || release.prerelease) continue; - let body = release.body; - - body = body.replace(/\r\n/g, "
"); - //replace ### title with h3 title h3 - body = body.replace( - /### (.*?)
/g, - '

$1

', - ); - body = body.replace(/<\/h3>
/gi, ""); - //remove - at the start of a line - body = body.replace(/^- /gm, ""); - //replace **bold** with bold - body = body.replace(/\*\*(.*?)\*\*/g, "$1"); - //replace links with a tags - body = body.replace( - /\[(.*?)\]\((.*?)\)/g, - '$1', - ); - - data.push({ - name: release.name, - publishedAt: format(new Date(release.published_at), "dd MMM yyyy"), - bodyHTML: body, - }); - } - return data; - }); + const releases = useQuery(() => ({ + ...getVersionHistoryQueryOptions(), + enabled: isOpen(), + })); return ( {(data) => ( diff --git a/frontend/src/ts/queries/public.ts b/frontend/src/ts/queries/public.ts index f997bbd18448..1743f3c52142 100644 --- a/frontend/src/ts/queries/public.ts +++ b/frontend/src/ts/queries/public.ts @@ -1,9 +1,14 @@ import { queryOptions } from "@tanstack/solid-query"; import { intervalToDuration } from "date-fns"; import Ape from "../ape"; -import { getContributorsList, getSupportersList } from "../utils/json-data"; +import { + getContributorsList, + getReleasesFromGitHub, + getSupportersList, +} from "../utils/json-data"; import { getNumberWithMagnitude, numberWithSpaces } from "../utils/numbers"; import { baseKey } from "./utils/keys"; +import { format as dateFormat } from "date-fns/format"; const queryKeys = { root: () => baseKey("public"), @@ -11,6 +16,7 @@ const queryKeys = { supporters: () => [...queryKeys.root(), "supporters"], typingStats: () => [...queryKeys.root(), "typingStats"], speedHistogram: () => [...queryKeys.root(), "speedHistogram"], + versionHistory: () => [...queryKeys.root(), "versionHistory"], }; //cache results for one hour @@ -48,6 +54,14 @@ export const getSpeedHistogramQueryOptions = () => staleTime, }); +// oxlint-disable-next-line typescript/explicit-function-return-type +export const getVersionHistoryQueryOptions = () => + queryOptions({ + queryKey: queryKeys.versionHistory(), + queryFn: fetchVersionHistory, + staleTime, + }); + async function fetchSpeedHistogram(): Promise< | { labels: string[]; @@ -145,3 +159,39 @@ async function fetchTypingStats(): Promise<{ }; return result; } + +async function fetchVersionHistory(): Promise< + { name: string; publishedAt: string; bodyHTML: string }[] +> { + const releases = await getReleasesFromGitHub(); + const data = []; + for (const release of releases) { + if (release.draft || release.prerelease) continue; + + let body = release.body; + + body = body.replace(/\r\n/g, "
"); + //replace ### title with h3 title h3 + body = body.replace( + /### (.*?)
/g, + '

$1

', + ); + body = body.replace(/<\/h3>
/gi, ""); + //remove - at the start of a line + body = body.replace(/^- /gm, ""); + //replace **bold** with bold + body = body.replace(/\*\*(.*?)\*\*/g, "$1"); + //replace links with a tags + body = body.replace( + /\[(.*?)\]\((.*?)\)/g, + '$1', + ); + + data.push({ + name: release.name, + publishedAt: dateFormat(new Date(release.published_at), "dd MMM yyyy"), + bodyHTML: body, + }); + } + return data; +} diff --git a/frontend/src/ts/utils/json-data.ts b/frontend/src/ts/utils/json-data.ts index eace5cfa19de..edb91981d9af 100644 --- a/frontend/src/ts/utils/json-data.ts +++ b/frontend/src/ts/utils/json-data.ts @@ -246,7 +246,7 @@ export async function getLatestReleaseFromGitHub(): Promise { * @returns A promise that resolves to the list of releases. */ export async function getReleasesFromGitHub(): Promise { - return cachedFetchJson( + return fetchJson( "https://api.github.com/repos/monkeytypegame/monkeytype/releases?per_page=5", ); } From 043e1e46d17f97d7c3f13fd23c2d71cfcf9c94fb Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 11 Feb 2026 12:13:10 +0100 Subject: [PATCH 2/4] infinite query --- frontend/src/ts/components/common/Button.tsx | 3 ++ .../components/modals/VersionHistoryModal.tsx | 32 ++++++++++++++++--- frontend/src/ts/queries/public.ts | 20 ++++++++---- frontend/src/ts/utils/json-data.ts | 6 ++-- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/frontend/src/ts/components/common/Button.tsx b/frontend/src/ts/components/common/Button.tsx index acd421c94a66..31e5713a040f 100644 --- a/frontend/src/ts/components/common/Button.tsx +++ b/frontend/src/ts/components/common/Button.tsx @@ -15,11 +15,13 @@ type ButtonProps = BaseProps & { onClick: () => void; href?: never; sameTarget?: true; + disabled?: boolean; }; type AnchorProps = BaseProps & { href: string; onClick?: never; + disabled?: never; }; export function Button(props: ButtonProps | AnchorProps): JSXElement { @@ -61,6 +63,7 @@ export function Button(props: ButtonProps | AnchorProps): JSXElement { type="button" classList={getClassList()} onClick={() => props.onClick?.()} + disabled={props.disabled ?? false} > {content} diff --git a/frontend/src/ts/components/modals/VersionHistoryModal.tsx b/frontend/src/ts/components/modals/VersionHistoryModal.tsx index 36e4087282f1..c67daec71646 100644 --- a/frontend/src/ts/components/modals/VersionHistoryModal.tsx +++ b/frontend/src/ts/components/modals/VersionHistoryModal.tsx @@ -1,15 +1,16 @@ -import { useQuery } from "@tanstack/solid-query"; +import { useInfiniteQuery } from "@tanstack/solid-query"; import { For, JSXElement } from "solid-js"; import { getVersionHistoryQueryOptions } from "../../queries/public"; import { isModalOpen } from "../../stores/modals"; import { AnimatedModal } from "../common/AnimatedModal"; import AsyncContent from "../common/AsyncContent"; +import { Button } from "../common/Button"; export function VersionHistoryModal(): JSXElement { const isOpen = (): boolean => isModalOpen("VersionHistory"); - const releases = useQuery(() => ({ + const releases = useInfiniteQuery(() => ({ ...getVersionHistoryQueryOptions(), enabled: isOpen(), })); @@ -21,9 +22,30 @@ export function VersionHistoryModal(): JSXElement { errorMessage="Failed to load version history" > {(data) => ( -
- {(release) => } -
+ <> +
+ it.releases)}> + {(release) => } + +
+ +