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
3 changes: 3 additions & 0 deletions frontend/src/ts/components/common/AnimatedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type AnimatedModalProps = ParentProps<{
afterHide?: () => void | Promise<void>;
onEscape?: (e: KeyboardEvent) => void;
onBackdropClick?: (e: MouseEvent) => void;
onScrollEnd?: (e: Event) => void;

title?: string;
modalClass?: string;
Expand Down Expand Up @@ -280,6 +281,8 @@ export function AnimatedModal(props: AnimatedModalProps): JSXElement {
props.modalClass,
)}
ref={modalRef}
// oxlint-disable-next-line react/no-unknown-property
onScrollEnd={(e) => props.onScrollEnd?.(e)}
>
<Show when={props.title !== undefined && props.title !== ""}>
<div class="text-2xl text-sub">{props.title}</div>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/ts/components/common/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -61,6 +63,7 @@ export function Button(props: ButtonProps | AnchorProps): JSXElement {
type="button"
classList={getClassList()}
onClick={() => props.onClick?.()}
disabled={props.disabled ?? false}
>
{content}
</button>
Expand Down
85 changes: 47 additions & 38 deletions frontend/src/ts/components/modals/VersionHistoryModal.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,67 @@
import { format } from "date-fns/format";
import { JSXElement, createResource, For } from "solid-js";
import { useInfiniteQuery } 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";
import { Button } from "../common/Button";

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;
const releases = useInfiniteQuery(() => ({
...getVersionHistoryQueryOptions(),
enabled: isOpen(),
}));

body = body.replace(/\r\n/g, "<br>");
//replace ### title with h3 title h3
body = body.replace(
/### (.*?)<br>/g,
'<h3 class="text-sub mb-2 text-xl">$1</h3>',
);
body = body.replace(/<\/h3><br>/gi, "</h3>");
//remove - at the start of a line
body = body.replace(/^- /gm, "");
//replace **bold** with bold
body = body.replace(/\*\*(.*?)\*\*/g, "<b>$1</b>");
//replace links with a tags
body = body.replace(
/\[(.*?)\]\((.*?)\)/g,
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
);
const scrollToLoadMore = (e: Event): void => {
const element = e.target as HTMLElement;

data.push({
name: release.name,
publishedAt: format(new Date(release.published_at), "dd MMM yyyy"),
bodyHTML: body,
});
if (
element.scrollHeight - element.scrollTop - element.clientHeight < 10 &&
releases.hasNextPage &&
!releases.isLoading
) {
void releases.fetchNextPage();
}
return data;
});
};

return (
<AnimatedModal id="VersionHistory" modalClass="max-w-6xl">
<AnimatedModal
id="VersionHistory"
modalClass="max-w-6xl"
onScrollEnd={scrollToLoadMore}
>
<AsyncContent
resource={releases}
query={releases}
errorMessage="Failed to load version history"
>
{(data) => (
<div class="releases">
<For each={data}>{(release) => <ReleaseItem {...release} />}</For>
</div>
<>
<div class="releases">
<For each={data.pages.flatMap((it) => it.releases)}>
{(release) => <ReleaseItem {...release} />}
</For>
</div>

<Button
onClick={() => void releases.fetchNextPage()}
disabled={!releases.hasNextPage || releases.isFetching}
fa={
releases.isFetchingNextPage
? { icon: "fa-circle-notch", spin: true, fixedWidth: true }
: undefined
}
text={
releases.isFetchingNextPage
? ""
: releases.hasNextPage
? "Load More"
: "Nothing more to load"
}
/>
</>
)}
</AsyncContent>
</AnimatedModal>
Expand Down
60 changes: 58 additions & 2 deletions frontend/src/ts/queries/public.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { queryOptions } from "@tanstack/solid-query";
import { infiniteQueryOptions, 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"),
contributors: () => [...queryKeys.root(), "contributors"],
supporters: () => [...queryKeys.root(), "supporters"],
typingStats: () => [...queryKeys.root(), "typingStats"],
speedHistogram: () => [...queryKeys.root(), "speedHistogram"],
versionHistory: () => [...queryKeys.root(), "versionHistory"],
};

//cache results for one hour
Expand Down Expand Up @@ -48,6 +54,16 @@ export const getSpeedHistogramQueryOptions = () =>
staleTime,
});

// oxlint-disable-next-line typescript/explicit-function-return-type
export const getVersionHistoryQueryOptions = () =>
infiniteQueryOptions({
queryKey: queryKeys.versionHistory(),
queryFn: fetchVersionHistory,
staleTime,
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: 1,
});

async function fetchSpeedHistogram(): Promise<
| {
labels: string[];
Expand Down Expand Up @@ -145,3 +161,43 @@ async function fetchTypingStats(): Promise<{
};
return result;
}

async function fetchVersionHistory(options: { pageParam: number }): Promise<{
nextCursor: number | undefined;
releases: { name: string; publishedAt: string; bodyHTML: string }[];
}> {
const releases = await getReleasesFromGitHub({ page: options.pageParam });
const data = [];
for (const release of releases) {
if (release.draft || release.prerelease) continue;

let body = release.body;

body = body.replace(/\r\n/g, "<br>");
//replace ### title with h3 title h3
body = body.replace(
/### (.*?)<br>/g,
'<h3 class="text-sub mb-2 text-xl">$1</h3>',
);
body = body.replace(/<\/h3><br>/gi, "</h3>");
//remove - at the start of a line
body = body.replace(/^- /gm, "");
//replace **bold** with bold
body = body.replace(/\*\*(.*?)\*\*/g, "<b>$1</b>");
//replace links with a tags
body = body.replace(
/\[(.*?)\]\((.*?)\)/g,
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
);

data.push({
name: release.name,
publishedAt: dateFormat(new Date(release.published_at), "dd MMM yyyy"),
bodyHTML: body,
});
}
return {
nextCursor: data.length > 0 ? options.pageParam + 1 : undefined,
releases: data,
};
}
8 changes: 5 additions & 3 deletions frontend/src/ts/utils/json-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,10 @@ export async function getLatestReleaseFromGitHub(): Promise<string> {
* Fetches the list of releases from GitHub.
* @returns A promise that resolves to the list of releases.
*/
export async function getReleasesFromGitHub(): Promise<GithubRelease[]> {
return cachedFetchJson(
"https://api.github.com/repos/monkeytypegame/monkeytype/releases?per_page=5",
export async function getReleasesFromGitHub(options?: {
page?: number;
}): Promise<GithubRelease[]> {
return fetchJson(
`https://api.github.com/repos/monkeytypegame/monkeytype/releases?per_page=5&page=${options?.page ?? 1}`,
);
}