From b912c28586f6552ea147b2256ab04c87f78da215 Mon Sep 17 00:00:00 2001 From: Daniel Jacobs Date: Wed, 7 May 2025 10:29:04 -0400 Subject: [PATCH 01/10] compatibility/avm2: Make files with English text client components --- src/app/compatibility/avm2/class_box.tsx | 2 +- src/app/compatibility/avm2/icons.tsx | 56 ++++++++++++++++++ src/app/compatibility/avm2/page.tsx | 27 +++++++-- src/app/compatibility/avm2/report_utils.tsx | 60 +------------------- src/app/compatibility/fetch-report/route.tsx | 25 ++++++++ 5 files changed, 107 insertions(+), 63 deletions(-) create mode 100644 src/app/compatibility/avm2/icons.tsx create mode 100644 src/app/compatibility/fetch-report/route.tsx diff --git a/src/app/compatibility/avm2/class_box.tsx b/src/app/compatibility/avm2/class_box.tsx index 0d68b691..f10c2fa3 100644 --- a/src/app/compatibility/avm2/class_box.tsx +++ b/src/app/compatibility/avm2/class_box.tsx @@ -14,9 +14,9 @@ import classes from "./avm2.module.css"; import React from "react"; import { ClassStatus, - ProgressIcon, displayedPercentage, } from "@/app/compatibility/avm2/report_utils"; +import { ProgressIcon } from "@/app/compatibility/avm2/icons"; export function ClassBox(props: ClassStatus) { const [opened, { toggle }] = useDisclosure(false); diff --git a/src/app/compatibility/avm2/icons.tsx b/src/app/compatibility/avm2/icons.tsx new file mode 100644 index 00000000..30ea2197 --- /dev/null +++ b/src/app/compatibility/avm2/icons.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { rem, ThemeIcon } from "@mantine/core"; +import { IconCheck, IconProgress, IconX } from "@tabler/icons-react"; + +export function IconDone() { + return ( + + + + ); +} + +export function IconStub() { + return ( + + + + ); +} + +export function IconMissing() { + return ( + + + + ); +} + +export function ProgressIcon(type: "stub" | "missing" | "done") { + switch (type) { + case "stub": + return ; + case "missing": + return ; + case "done": + return ; + } +} diff --git a/src/app/compatibility/avm2/page.tsx b/src/app/compatibility/avm2/page.tsx index 0b1d447c..4f0ced60 100644 --- a/src/app/compatibility/avm2/page.tsx +++ b/src/app/compatibility/avm2/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Container, Group, @@ -8,16 +10,18 @@ import { Title, } from "@mantine/core"; import Image from "next/image"; -import React from "react"; +import React, { useEffect, useState } from "react"; import classes from "./avm2.module.css"; import { ClassBox } from "@/app/compatibility/avm2/class_box"; import { getReportByNamespace, + NamespaceStatus, +} from "@/app/compatibility/avm2/report_utils"; +import { IconDone, IconMissing, IconStub, - NamespaceStatus, -} from "@/app/compatibility/avm2/report_utils"; +} from "@/app/compatibility/avm2/icons"; import Link from "next/link"; function NamespaceBox(props: NamespaceStatus) { @@ -33,8 +37,21 @@ function NamespaceBox(props: NamespaceStatus) { ); } -export default async function Page() { - const byNamespace = await getReportByNamespace(); +export default function Page() { + const [byNamespace, setByNamespace] = useState< + { [name: string]: NamespaceStatus } | undefined + >(undefined); + useEffect(() => { + const fetchData = async () => { + try { + const byNamespace = await getReportByNamespace(); + setByNamespace(byNamespace); + } catch (error) { + console.error("Error fetching data", error); + } + }; + fetchData(); + }, []); return ( diff --git a/src/app/compatibility/avm2/report_utils.tsx b/src/app/compatibility/avm2/report_utils.tsx index 73ebfb07..8ccb40d1 100644 --- a/src/app/compatibility/avm2/report_utils.tsx +++ b/src/app/compatibility/avm2/report_utils.tsx @@ -1,59 +1,4 @@ -import { rem, ThemeIcon } from "@mantine/core"; -import { IconCheck, IconProgress, IconX } from "@tabler/icons-react"; -import { fetchReport } from "@/app/downloads/github"; -import React from "react"; - -export function IconDone() { - return ( - - - - ); -} - -export function IconStub() { - return ( - - - - ); -} - -export function IconMissing() { - return ( - - - - ); -} - -export function ProgressIcon(type: "stub" | "missing" | "done") { - switch (type) { - case "stub": - return ; - case "missing": - return ; - case "done": - return ; - } -} +import type { AVM2Report } from "@/app/downloads/config"; export interface SummaryStatistics { max_points: number; @@ -82,7 +27,8 @@ export async function getReportByNamespace(): Promise< { [name: string]: NamespaceStatus } | undefined > { let byNamespace: { [name: string]: NamespaceStatus } = {}; - const report = await fetchReport(); + const reportReq = await fetch("/compatibility/fetch-report"); + const report: AVM2Report = await reportReq.json(); if (!report) { return; } diff --git a/src/app/compatibility/fetch-report/route.tsx b/src/app/compatibility/fetch-report/route.tsx new file mode 100644 index 00000000..9111b48c --- /dev/null +++ b/src/app/compatibility/fetch-report/route.tsx @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import { fetchReport } from "@/app/downloads/github"; +import { AVM2Report } from "@/app/downloads/config"; + +let cachedReport: AVM2Report | undefined; + +export async function GET() { + if (cachedReport) { + return NextResponse.json(cachedReport); // Return cached result + } + + try { + const report = await fetchReport(); + cachedReport = report; // Cache the result + return NextResponse.json(report); + } catch (error) { + console.error("Error fetching report:", error); + return NextResponse.json( + { error: "Failed to fetch report" }, + { status: 500 }, + ); + } +} + +export const dynamic = "force-static"; From 8d5e18d287c8c5b8f3c509926e593d77f4b5b29b Mon Sep 17 00:00:00 2001 From: Daniel Jacobs Date: Wed, 7 May 2025 12:03:17 -0400 Subject: [PATCH 02/10] compatibility: Make files with English text client components --- src/app/compatibility/avm.tsx | 9 ++++- src/app/compatibility/page.tsx | 73 ++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/app/compatibility/avm.tsx b/src/app/compatibility/avm.tsx index 0acb110a..20f79712 100644 --- a/src/app/compatibility/avm.tsx +++ b/src/app/compatibility/avm.tsx @@ -1,3 +1,5 @@ +"use client"; + import classes from "./avm.module.css"; import { Button, @@ -24,7 +26,10 @@ function AvmProgress(props: AvmProgressPropsFull) { return ( - {props.name}: {props.done}% + {props.name}:{" "} + {typeof props.done === "number" && props.done > 0 + ? `${props.done}%` + : "Loading..."} - {props.stubbed && ( + {typeof props.stubbed === "number" && props.stubbed > 0 && ( { - return { - week: new Date(item.week * 1000).toISOString().split("T")[0], - Commits: item.total, +interface DataPoint { + week: string; + Commits: number; +} + +export default function Downloads() { + const [data, setData] = useState([]); + const [avm1ApiDone, setAvm1ApiDone] = useState(0); + const [avm2ApiDone, setAvm2ApiDone] = useState(0); + const [avm2ApiStubbed, setAvm2ApiStubbed] = useState(0); + useEffect(() => { + const fetchData = async () => { + try { + // Fetch weekly contributions + const contributionsRes = await getWeeklyContributions(); + const contributionsData = contributionsRes.data.map((item) => ({ + week: new Date(item.week * 1000).toISOString().split("T")[0], + Commits: item.total, + })); + setData(contributionsData); + + // Fetch AVM1 progress + const avm1ApiRes = await getAVM1Progress(); + setAvm1ApiDone(avm1ApiRes); + + // Fetch report + const reportReq = await fetch("/compatibility/fetch-report"); + const reportRes = await reportReq.json(); + if (reportRes) { + const { summary } = reportRes; + const maxPoints = summary.max_points; + const implPoints = summary.impl_points; + const stubPenalty = summary.stub_penalty; + + const avm2ApiDone = Math.round( + ((implPoints - stubPenalty) / maxPoints) * 100, + ); + setAvm2ApiDone(avm2ApiDone); + + const avm2ApiStubbed = Math.round((stubPenalty / maxPoints) * 100); + setAvm2ApiStubbed(avm2ApiStubbed); + } + } catch (error) { + console.error("Error fetching data", error); + } }; - }); - const avm1ApiDone = await getAVM1Progress(); - const report = await fetchReport(); - if (!report) { - return; - } - const summary = report.summary; - const maxPoints = summary.max_points; - const implPoints = summary.impl_points; - const stubPenalty = summary.stub_penalty; - const avm2ApiDone = Math.round( - ((implPoints - stubPenalty) / maxPoints) * 100, - ); - const avm2ApiStubbed = Math.round((stubPenalty / maxPoints) * 100); + + fetchData(); + }, []); return ( From 78cac034b75496ac7117d36731ccd5a934bf227a Mon Sep 17 00:00:00 2001 From: Daniel Jacobs Date: Wed, 7 May 2025 12:07:43 -0400 Subject: [PATCH 03/10] contribute: Make files with English text client components --- src/app/contribute/page.tsx | 2 ++ src/app/contribute/sponsors.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/app/contribute/page.tsx b/src/app/contribute/page.tsx index 44f9c9ef..c5625c24 100644 --- a/src/app/contribute/page.tsx +++ b/src/app/contribute/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Container, Group, diff --git a/src/app/contribute/sponsors.tsx b/src/app/contribute/sponsors.tsx index 88037c10..549190a6 100644 --- a/src/app/contribute/sponsors.tsx +++ b/src/app/contribute/sponsors.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Card, Group, Stack, Text, Title } from "@mantine/core"; import classes from "./sponsors.module.css"; import React from "react"; From 9083bbdb685ff120cb681f415d4f1bc42091ab30 Mon Sep 17 00:00:00 2001 From: Daniel Jacobs Date: Wed, 7 May 2025 12:17:54 -0400 Subject: [PATCH 04/10] downloads: Make files with English text client components --- src/app/downloads/extensions.tsx | 2 ++ src/app/downloads/page.tsx | 28 +++++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/app/downloads/extensions.tsx b/src/app/downloads/extensions.tsx index 95784279..b818a67a 100644 --- a/src/app/downloads/extensions.tsx +++ b/src/app/downloads/extensions.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Group, Stack, Text, Title } from "@mantine/core"; import classes from "./extensions.module.css"; import React from "react"; diff --git a/src/app/downloads/page.tsx b/src/app/downloads/page.tsx index 622d6cc7..71c4e645 100644 --- a/src/app/downloads/page.tsx +++ b/src/app/downloads/page.tsx @@ -1,3 +1,6 @@ +"use client"; + +import React, { useState, useEffect } from "react"; import { Button, Code, @@ -8,7 +11,6 @@ import { Title, } from "@mantine/core"; import classes from "./downloads.module.css"; -import React from "react"; import { ExtensionList } from "@/app/downloads/extensions"; import { NightlyList } from "@/app/downloads/nightlies"; import Link from "next/link"; @@ -93,12 +95,24 @@ function DesktopDownload({ latest }: { latest: GithubRelease | null }) { ); } -export default async function Page() { - const releases = await getLatestReleases(); - const latest = releases.length > 0 ? releases[0] : null; - const nightlies = releases - .filter((release) => release.prerelease) - .slice(0, maxNightlies); +export default function Page() { + const [latest, setLatest] = useState(null); + const [nightlies, setNightlies] = useState([]); + useEffect(() => { + const fetchReleases = async () => { + try { + const releases = await getLatestReleases(); + const nightlies = releases + .filter((release) => release.prerelease) + .slice(0, maxNightlies); + setNightlies(nightlies); + setLatest(releases.length > 0 ? releases[0] : null); + } catch (err) { + console.warn("Failed to fetch releases", err); + } + }; + fetchReleases(); + }, []); return ( From 7682dd784f9ce9c2af0706047972d88d57195b0f Mon Sep 17 00:00:00 2001 From: Daniel Jacobs Date: Wed, 7 May 2025 12:19:55 -0400 Subject: [PATCH 05/10] 404: Make page with English text client component --- src/app/not-found.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index a116d776..4a569bcc 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,3 +1,5 @@ +"use client"; + import classes from "./not-found.module.css"; import { Stack, Text, Title } from "@mantine/core"; import React from "react"; From 70b2c94158dd05ae196c2f95bfe53b134b5cf621 Mon Sep 17 00:00:00 2001 From: Daniel Jacobs Date: Wed, 7 May 2025 12:21:46 -0400 Subject: [PATCH 06/10] Footer: Make client component --- src/components/footer.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/footer.tsx b/src/components/footer.tsx index 3c5c7eef..507e6190 100644 --- a/src/components/footer.tsx +++ b/src/components/footer.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Container, Group, ActionIcon, rem, Text } from "@mantine/core"; import Link from "next/link"; From f8dcb10e1df4d6aa5bd465a6ceb60b0bff25975f Mon Sep 17 00:00:00 2001 From: Daniel Jacobs Date: Wed, 7 May 2025 12:33:13 -0400 Subject: [PATCH 07/10] logo: Make into functional component instead of class component --- src/components/logo.tsx | 105 ++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 63 deletions(-) diff --git a/src/components/logo.tsx b/src/components/logo.tsx index b950a93c..67393270 100644 --- a/src/components/logo.tsx +++ b/src/components/logo.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import Image from "next/image"; import Script from "next/script"; import classes from "../app/index.module.css"; @@ -27,44 +27,29 @@ interface LogoProps { className?: string; } -interface LogoState { - player: RufflePlayer | null; -} - -export default class InteractiveLogo extends React.Component< - LogoProps, - LogoState -> { - private readonly container: React.RefObject; - private player: RufflePlayer | null = null; +export default function InteractiveLogo({ className }: LogoProps) { + const container = useRef(null); + const [player, setPlayer] = useState(null); - constructor(props: LogoProps) { - super(props); - - this.container = React.createRef(); - this.state = { - player: null, - }; - } + const removeRufflePlayer = () => { + player?.remove(); + setPlayer(null); + }; - private removeRufflePlayer() { - this.player?.remove(); - this.player = null; - this.setState({ player: null }); - } - - private load() { - if (this.player) { + const loadPlayer = () => { + if (player) { // Already loaded. return; } - this.player = (window.RufflePlayer as PublicAPI)?.newest()?.createPlayer(); + const rufflePlayer = (window.RufflePlayer as PublicAPI) + ?.newest() + ?.createPlayer(); - if (this.player) { - this.container.current!.appendChild(this.player); + if (rufflePlayer) { + container.current!.appendChild(rufflePlayer); - this.player + rufflePlayer .load({ url: "/logo-anim.swf", autoplay: "on", @@ -75,39 +60,33 @@ export default class InteractiveLogo extends React.Component< preferredRenderer: "canvas", }) .catch(() => { - this.removeRufflePlayer(); + removeRufflePlayer(); }); - this.player.style.width = "100%"; - this.player.style.height = "100%"; - this.setState({ player: this.player }); + rufflePlayer.style.width = "100%"; + rufflePlayer.style.height = "100%"; + setPlayer(rufflePlayer); } - } - - componentDidMount() { - this.load(); - } - - componentWillUnmount() { - this.removeRufflePlayer(); - } - - render() { - return ( - <> -