From 4ef64cbebcf92d25f2f65f1905272291316e0720 Mon Sep 17 00:00:00 2001 From: Ethan Roseman Date: Wed, 11 Dec 2024 01:03:56 +0900 Subject: [PATCH] Run Biome formatter (#1392) * Run formatter * Part 2 --- frontend/biome.json | 5 - frontend/next.config.js | 289 ++++--- frontend/postcss.config.js | 8 +- frontend/src/app/(navfooter)/SiteStats.tsx | 46 +- frontend/src/app/(navfooter)/WelcomeInfo.tsx | 97 ++- .../(navfooter)/credits/ContributorsList.tsx | 99 ++- .../src/app/(navfooter)/credits/LinkList.tsx | 26 +- frontend/src/app/(navfooter)/credits/page.tsx | 143 ++-- frontend/src/app/(navfooter)/error.tsx | 6 +- frontend/src/app/(navfooter)/faq/page.tsx | 305 ++++--- frontend/src/app/(navfooter)/layout.tsx | 28 +- frontend/src/app/(navfooter)/login/page.tsx | 145 ++-- .../app/(navfooter)/new/NewScratchForm.tsx | 489 ++++++----- .../src/app/(navfooter)/new/description.ts | 5 +- frontend/src/app/(navfooter)/new/layout.tsx | 10 +- frontend/src/app/(navfooter)/new/page.tsx | 24 +- frontend/src/app/(navfooter)/page.tsx | 48 +- .../app/(navfooter)/platform/[id]/page.tsx | 81 +- .../src/app/(navfooter)/preset/[id]/page.tsx | 87 +- frontend/src/app/(navfooter)/preset/page.tsx | 10 +- .../src/app/(navfooter)/preset/presets.tsx | 39 +- frontend/src/app/(navfooter)/privacy/page.tsx | 245 +++--- .../src/app/(navfooter)/settings/Checkbox.tsx | 60 +- .../src/app/(navfooter)/settings/NavItem.tsx | 46 +- .../app/(navfooter)/settings/RadioList.tsx | 106 ++- .../src/app/(navfooter)/settings/Section.tsx | 22 +- .../app/(navfooter)/settings/SliderField.tsx | 120 +-- .../app/(navfooter)/settings/TextField.tsx | 78 +- .../settings/account/ProfileSection.tsx | 47 +- .../settings/account/SignOutButton.tsx | 32 +- .../settings/account/UserState.tsx | 55 +- .../app/(navfooter)/settings/account/page.tsx | 22 +- .../appearance/AppearanceSettings.tsx | 128 +-- .../settings/appearance/ExampleCodeMirror.tsx | 28 +- .../(navfooter)/settings/appearance/page.tsx | 12 +- .../settings/editor/EditorSettings.tsx | 236 +++--- .../app/(navfooter)/settings/editor/page.tsx | 12 +- .../src/app/(navfooter)/settings/layout.tsx | 48 +- .../src/app/(navfooter)/u/[username]/page.tsx | 38 +- frontend/src/app/ThemeProvider.tsx | 56 +- frontend/src/app/error.tsx | 136 +-- frontend/src/app/layout.tsx | 70 +- frontend/src/app/not-found.tsx | 45 +- frontend/src/app/opengraph-image.tsx | 112 ++- .../src/app/scratch/[slug]/ScratchEditor.tsx | 144 ++-- .../src/app/scratch/[slug]/claim/page.tsx | 49 +- .../app/scratch/[slug]/getScratchDetails.ts | 24 +- frontend/src/app/scratch/[slug]/loading.tsx | 148 ++-- .../app/scratch/[slug]/opengraph-image.tsx | 149 ++-- frontend/src/app/scratch/[slug]/page.tsx | 36 +- frontend/src/components/AsyncButton.tsx | 131 +-- frontend/src/components/Breadcrumbs.tsx | 43 +- frontend/src/components/Button.tsx | 106 +-- frontend/src/components/ColorSchemeEditor.tsx | 116 +-- frontend/src/components/ColorSchemePicker.tsx | 75 +- frontend/src/components/CustomLayout.tsx | 119 +-- .../src/components/Diff/CompilationPanel.tsx | 235 +++--- frontend/src/components/Diff/Diff.tsx | 409 +++++---- .../src/components/Diff/DiffRowAsmDiffer.tsx | 217 +++-- .../src/components/Diff/DiffRowObjdiff.tsx | 407 +++++---- frontend/src/components/Diff/DragBar.tsx | 68 +- frontend/src/components/Diff/Highlighter.tsx | 48 +- frontend/src/components/DismissableBanner.tsx | 36 +- frontend/src/components/Editor/CodeMirror.tsx | 202 +++-- frontend/src/components/ErrorBoundary.tsx | 32 +- frontend/src/components/Footer.tsx | 118 +-- frontend/src/components/GhostButton.tsx | 50 +- frontend/src/components/GitHubLoginButton.tsx | 40 +- frontend/src/components/Logotype.tsx | 17 +- frontend/src/components/Nav/LoginState.tsx | 79 +- frontend/src/components/Nav/Nav.tsx | 88 +- frontend/src/components/Nav/Search.tsx | 340 ++++---- frontend/src/components/Nav/UserMenuItems.tsx | 68 +- frontend/src/components/Nav/index.tsx | 4 +- frontend/src/components/NumberInput.tsx | 103 ++- frontend/src/components/PlatformLink.tsx | 30 +- .../PlatformSelect/PlatformIcon.tsx | 92 +- .../PlatformSelect/PlatformName.tsx | 25 +- .../PlatformSelect/PlatformSelect.tsx | 68 +- .../PlatformSelect/ScrollingPlatformIcons.tsx | 30 +- .../src/components/PlatformSelect/index.ts | 8 +- frontend/src/components/PresetList.tsx | 126 ++- frontend/src/components/ScoreBadge.tsx | 89 +- frontend/src/components/Scratch/Scratch.tsx | 563 ++++++++----- .../components/Scratch/ScratchMatchBanner.tsx | 50 +- .../components/Scratch/ScratchProgressBar.tsx | 25 +- .../src/components/Scratch/ScratchToolbar.tsx | 464 +++++----- .../components/Scratch/SortableFamilyList.tsx | 150 ++-- .../Scratch/hooks/useFuzzySaveCallback.ts | 30 +- .../Scratch/hooks/useLanguageServer.ts | 149 ++-- .../hooks/useWarnBeforeScratchUnload.ts | 10 +- frontend/src/components/Scratch/index.ts | 6 +- .../components/Scratch/panels/AboutPanel.tsx | 131 +-- .../Scratch/panels/DecompilePanel.tsx | 98 +-- .../components/Scratch/panels/FamilyPanel.tsx | 33 +- frontend/src/components/ScratchList.tsx | 288 ++++--- frontend/src/components/ScrollContext.tsx | 4 +- frontend/src/components/Select.tsx | 39 +- frontend/src/components/Select2.tsx | 54 +- frontend/src/components/SetPageTitle.tsx | 8 +- frontend/src/components/Shortcut.tsx | 173 ++-- frontend/src/components/Sort.tsx | 65 +- frontend/src/components/Tabs.tsx | 358 ++++---- frontend/src/components/ThemePicker.tsx | 68 +- frontend/src/components/TimeAgo.tsx | 21 +- frontend/src/components/VerticalMenu.tsx | 210 +++-- frontend/src/components/YourScratchList.tsx | 29 +- .../src/components/compiler/CompilerOpts.tsx | 791 +++++++++++------- .../src/components/compiler/PresetSelect.tsx | 84 +- frontend/src/components/compiler/compilers.ts | 19 +- .../src/components/user/AnonymousFrog.tsx | 41 +- frontend/src/components/user/Profile.tsx | 91 +- frontend/src/components/user/UserAvatar.tsx | 54 +- frontend/src/components/user/UserLink.tsx | 38 +- frontend/src/components/user/UserMention.tsx | 62 +- .../src/components/user/tabs/ScratchesTab.tsx | 8 +- frontend/src/lib/api.ts | 492 ++++++----- frontend/src/lib/api/request.ts | 102 +-- frontend/src/lib/api/types.ts | 232 ++--- frontend/src/lib/api/urls.ts | 21 +- frontend/src/lib/codemirror/basic-setup.ts | 37 +- frontend/src/lib/codemirror/color-scheme.ts | 48 +- frontend/src/lib/codemirror/cpp.ts | 31 +- .../lib/codemirror/default-keymap/indent.ts | 18 +- .../lib/codemirror/default-keymap/index.ts | 38 +- .../codemirror/default-keymap/multiCursor.ts | 52 +- frontend/src/lib/codemirror/default-theme.ts | 76 +- frontend/src/lib/codemirror/languageServer.ts | 650 +++++++------- .../src/lib/codemirror/useCompareExtension.ts | 163 ++-- .../codemirror/useCompareExtension.worker.ts | 21 +- frontend/src/lib/device.ts | 10 +- frontend/src/lib/hooks.ts | 111 +-- frontend/src/lib/i18n/translate.ts | 26 +- frontend/src/lib/interdiff.ts | 94 ++- frontend/src/lib/oauth.ts | 31 +- frontend/src/lib/objdiff.ts | 22 +- frontend/src/lib/settings.ts | 80 +- frontend/src/lib/title.ts | 5 +- frontend/src/misc_types.d.ts | 16 +- frontend/tailwind.config.js | 49 +- 140 files changed, 8309 insertions(+), 6243 deletions(-) diff --git a/frontend/biome.json b/frontend/biome.json index d7f2f35b1..87f234e4e 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -48,10 +48,5 @@ "useSortedClasses": "error" } } - }, - "javascript": { - "formatter": { - "enabled": false - } } } diff --git a/frontend/next.config.js b/frontend/next.config.js index fb8c8b2ec..aa864aa46 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -1,168 +1,185 @@ -const { Compilation } = require("webpack") -const { execSync } = require("node:child_process") -const { config } = require("dotenv") +const { Compilation } = require("webpack"); +const { execSync } = require("node:child_process"); +const { config } = require("dotenv"); for (const envFile of [".env.local", ".env"]) { - config({ path: `../${envFile}` }) + config({ path: `../${envFile}` }); } const getEnvBool = (key, fallback = false) => { - const value = process.env[key] + const value = process.env[key]; if (value === "false" || value === "0" || value === "off") { - return false + return false; } if (value === "true" || value === "1" || value === "on") { - return true + return true; } - return fallback -} + return fallback; +}; -let git_hash +let git_hash; try { - git_hash = execSync("git rev-parse HEAD", { stdio: "pipe" }).toString().trim() + git_hash = execSync("git rev-parse HEAD", { stdio: "pipe" }) + .toString() + .trim(); } catch (error) { - console.log("Unable to get git hash, assume running inside Docker") - git_hash = "abc123" + console.log("Unable to get git hash, assume running inside Docker"); + git_hash = "abc123"; } -const { withPlausibleProxy } = require("next-plausible") +const { withPlausibleProxy } = require("next-plausible"); const removeImports = require("next-remove-imports")({ //test: /node_modules([\s\S]*?)\.(tsx|ts|js|mjs|jsx)$/, //matchImports: "\\.(less|css|scss|sass|styl)$" -}) +}); -const mediaUrl = new URL(process.env.MEDIA_URL ?? "http://localhost") +const mediaUrl = new URL(process.env.MEDIA_URL ?? "http://localhost"); let app = withPlausibleProxy({ customDomain: "https://stats.decomp.me", -})(removeImports({ - async redirects() { - return [ - { - source: "/scratch", - destination: "/", - permanent: true, - }, - { - source: "/scratch/new", - destination: "/new", - permanent: true, - }, - { - source: "/settings", - destination: "/settings/account", - permanent: false, - }, - ] - }, - async rewrites() { - return [] - }, - async headers() { - return [ - { - source: "/(.*)", // all routes - headers: [ - { - key: "X-DNS-Prefetch-Control", - value: "on", - }, - { - key: "Cross-Origin-Opener-Policy", - value: "same-origin", - }, - { - key: "Cross-Origin-Embedder-Policy", - value: "require-corp", - }, - ], - }, - ] - }, - webpack(config) { - config.module.rules.push({ - test: /\.svg$/, - use: ["@svgr/webpack"], - }) +})( + removeImports({ + async redirects() { + return [ + { + source: "/scratch", + destination: "/", + permanent: true, + }, + { + source: "/scratch/new", + destination: "/new", + permanent: true, + }, + { + source: "/settings", + destination: "/settings/account", + permanent: false, + }, + ]; + }, + async rewrites() { + return []; + }, + async headers() { + return [ + { + source: "/(.*)", // all routes + headers: [ + { + key: "X-DNS-Prefetch-Control", + value: "on", + }, + { + key: "Cross-Origin-Opener-Policy", + value: "same-origin", + }, + { + key: "Cross-Origin-Embedder-Policy", + value: "require-corp", + }, + ], + }, + ]; + }, + webpack(config) { + config.module.rules.push({ + test: /\.svg$/, + use: ["@svgr/webpack"], + }); - // @open-rpc/client-js brings in some dependencies which, in turn, have optional dependencies. - // This confuses the heck out of webpack, so tell it should just sub in a CommonJS-style "require" statement - // instead (which will fail and trigger the fallback at runtime) - // https://stackoverflow.com/questions/58697934/webpack-how-do-you-require-an-optional-dependency-in-bundle-saslprep - config.externals.push({ - "encoding": "commonjs encoding", - "bufferutil": "commonjs bufferutil", - "utf-8-validate": "commonjs utf-8-validate", - }) + // @open-rpc/client-js brings in some dependencies which, in turn, have optional dependencies. + // This confuses the heck out of webpack, so tell it should just sub in a CommonJS-style "require" statement + // instead (which will fail and trigger the fallback at runtime) + // https://stackoverflow.com/questions/58697934/webpack-how-do-you-require-an-optional-dependency-in-bundle-saslprep + config.externals.push({ + encoding: "commonjs encoding", + bufferutil: "commonjs bufferutil", + "utf-8-validate": "commonjs utf-8-validate", + }); - // All of the vscode-* packages (jsonrpc, languageserver-protocol, etc.) are distributed as UMD modules. - // This also leaves webpack with no idea how to handle require statements. - // umd-compat-loader strips away the hedaer UMD adds to allow browsers to parse ES modules - // and just treats the importee as an ES module. - config.module.rules.push({ - "test": /node_modules[\\|/](vscode-.*)/, - "use": { - "loader": "umd-compat-loader", - }, - }) + // All of the vscode-* packages (jsonrpc, languageserver-protocol, etc.) are distributed as UMD modules. + // This also leaves webpack with no idea how to handle require statements. + // umd-compat-loader strips away the hedaer UMD adds to allow browsers to parse ES modules + // and just treats the importee as an ES module. + config.module.rules.push({ + test: /node_modules[\\|/](vscode-.*)/, + use: { + loader: "umd-compat-loader", + }, + }); - // XXX: Terser/SWC currently breaks while minifying objdiff's ESM worker in the static directory because - // it uses `import.meta.url`. next.js provides no way to control this behavior, of course, so we'll - // hook into the optimization stage and mark assets in `static/media/` as already minified. - // https://github.com/vercel/next.js/issues/33914 - // https://github.com/vercel/next.js/discussions/61549 - config.optimization.minimizer.unshift({ - apply(compiler) { - const pluginName = "SkipWorkerMinify" - compiler.hooks.thisCompilation.tap(pluginName, compilation=>{ - compilation.hooks.processAssets.tap({ - name: pluginName, - stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, - }, assets=>{ - for (const assetName in assets) { - if (/^static\/media\//.test(assetName)) { - compilation.updateAsset(assetName, assets[assetName], { - minimized: true, - }) - } - } - }) - }) - }, - }) + // XXX: Terser/SWC currently breaks while minifying objdiff's ESM worker in the static directory because + // it uses `import.meta.url`. next.js provides no way to control this behavior, of course, so we'll + // hook into the optimization stage and mark assets in `static/media/` as already minified. + // https://github.com/vercel/next.js/issues/33914 + // https://github.com/vercel/next.js/discussions/61549 + config.optimization.minimizer.unshift({ + apply(compiler) { + const pluginName = "SkipWorkerMinify"; + compiler.hooks.thisCompilation.tap( + pluginName, + (compilation) => { + compilation.hooks.processAssets.tap( + { + name: pluginName, + stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, + }, + (assets) => { + for (const assetName in assets) { + if ( + /^static\/media\//.test(assetName) + ) { + compilation.updateAsset( + assetName, + assets[assetName], + { + minimized: true, + }, + ); + } + } + }, + ); + }, + ); + }, + }); - return config - }, - images: { - remotePatterns: [{ - // Expected 'http' | 'https', received 'http:' at "images.remotePatterns[0].protocol" - protocol: mediaUrl.protocol.replace(":", ""), - hostname: mediaUrl.hostname, - port: mediaUrl.port, - pathname: "/**", + return config; + }, + images: { + remotePatterns: [ + { + // Expected 'http' | 'https', received 'http:' at "images.remotePatterns[0].protocol" + protocol: mediaUrl.protocol.replace(":", ""), + hostname: mediaUrl.hostname, + port: mediaUrl.port, + pathname: "/**", + }, + { + protocol: "https", + hostname: "avatars.githubusercontent.com", + port: "", + pathname: "/**", + }, + ], + unoptimized: !getEnvBool("FRONTEND_USE_IMAGE_PROXY"), }, - { - protocol: "https", - hostname: "avatars.githubusercontent.com", - port: "", - pathname: "/**", + swcMinify: true, + env: { + // XXX: don't need 'NEXT_PUBLIC_' prefix here; we could just use 'API_BASE' and 'GITHUB_CLIENT_ID' + // See note at top of https://nextjs.org/docs/api-reference/next.config.js/environment-variables for more information + NEXT_PUBLIC_API_BASE: process.env.API_BASE, + NEXT_PUBLIC_GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, + NEXT_PUBLIC_COMMIT_HASH: git_hash, }, - ], - unoptimized: !getEnvBool("FRONTEND_USE_IMAGE_PROXY"), - }, - swcMinify: true, - env: { - // XXX: don't need 'NEXT_PUBLIC_' prefix here; we could just use 'API_BASE' and 'GITHUB_CLIENT_ID' - // See note at top of https://nextjs.org/docs/api-reference/next.config.js/environment-variables for more information - NEXT_PUBLIC_API_BASE: process.env.API_BASE, - NEXT_PUBLIC_GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, - NEXT_PUBLIC_COMMIT_HASH: git_hash, - }, -})) + }), +); if (process.env.ANALYZE === "true") { - app = require("@next/bundle-analyzer")(app) + app = require("@next/bundle-analyzer")(app); } -module.exports = app +module.exports = app; diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index dd943edf5..2c5fea0c5 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,7 +1,3 @@ module.exports = { - plugins: [ - "tailwindcss", - "autoprefixer", - "cssnano", - ], -} + plugins: ["tailwindcss", "autoprefixer", "cssnano"], +}; diff --git a/frontend/src/app/(navfooter)/SiteStats.tsx b/frontend/src/app/(navfooter)/SiteStats.tsx index 62efadb4d..dd8800478 100644 --- a/frontend/src/app/(navfooter)/SiteStats.tsx +++ b/frontend/src/app/(navfooter)/SiteStats.tsx @@ -1,34 +1,42 @@ -"use client" +"use client"; -import type { ReactNode } from "react" +import type { ReactNode } from "react"; -import Link from "next/link" +import Link from "next/link"; -import { useStats } from "@/lib/api" +import { useStats } from "@/lib/api"; -function Stat({ children, href }: { children: ReactNode, href?: string }) { +function Stat({ children, href }: { children: ReactNode; href?: string }) { if (href) { - return - {children} - + return ( + + {children} + + ); } - return - {children} - + return {children}; } export default function SiteStats() { - const stats = useStats() + const stats = useStats(); if (!stats) { - return null + return null; } - return

- {stats.scratch_count.toLocaleString()} scratches created - {stats.profile_count.toLocaleString()} unique visitors - {stats.github_user_count.toLocaleString()} users signed up - {stats.asm_count.toLocaleString()} asm globs submitted -

+ return ( +

+ + {stats.scratch_count.toLocaleString()} scratches created + + + {stats.profile_count.toLocaleString()} unique visitors + + + {stats.github_user_count.toLocaleString()} users signed up + + {stats.asm_count.toLocaleString()} asm globs submitted +

+ ); } diff --git a/frontend/src/app/(navfooter)/WelcomeInfo.tsx b/frontend/src/app/(navfooter)/WelcomeInfo.tsx index c03ba033d..58e9b5636 100644 --- a/frontend/src/app/(navfooter)/WelcomeInfo.tsx +++ b/frontend/src/app/(navfooter)/WelcomeInfo.tsx @@ -1,55 +1,62 @@ -import { headers } from "next/headers" -import Link from "next/link" +import { headers } from "next/headers"; +import Link from "next/link"; -import { ArrowRightIcon } from "@primer/octicons-react" +import { ArrowRightIcon } from "@primer/octicons-react"; -import Button from "@/components/Button" -import GitHubLoginButton from "@/components/GitHubLoginButton" -import ScrollingPlatformIcons from "@/components/PlatformSelect/ScrollingPlatformIcons" +import Button from "@/components/Button"; +import GitHubLoginButton from "@/components/GitHubLoginButton"; +import ScrollingPlatformIcons from "@/components/PlatformSelect/ScrollingPlatformIcons"; -import SiteStats from "./SiteStats" +import SiteStats from "./SiteStats"; -export const SITE_DESCRIPTION = "A collaborative reverse-engineering platform for working on decompilation projects with others to learn about how your favorite games work." +export const SITE_DESCRIPTION = + "A collaborative reverse-engineering platform for working on decompilation projects with others to learn about how your favorite games work."; export default function WelcomeInfo() { - const saveDataEnabled = headers().get("Save-Data") === "on" + const saveDataEnabled = headers().get("Save-Data") === "on"; - return
- {!saveDataEnabled &&
- -
-
} -
-

- Collaboratively decompile code in your browser. -

-

- {SITE_DESCRIPTION} -

-
- - - - -
-
- + return ( +
+ {!saveDataEnabled && ( +
+ +
+
+ )} +
+

+ Collaboratively decompile code in your browser. +

+

+ {SITE_DESCRIPTION} +

+
+ + + + +
+
+ +
-
+ ); } diff --git a/frontend/src/app/(navfooter)/credits/ContributorsList.tsx b/frontend/src/app/(navfooter)/credits/ContributorsList.tsx index 72ff557b4..20a6bc86b 100644 --- a/frontend/src/app/(navfooter)/credits/ContributorsList.tsx +++ b/frontend/src/app/(navfooter)/credits/ContributorsList.tsx @@ -1,68 +1,89 @@ -import { LinkExternalIcon } from "@primer/octicons-react" +import { LinkExternalIcon } from "@primer/octicons-react"; -import GhostButton from "@/components/GhostButton" -import UserAvatar from "@/components/user/UserAvatar" -import UserMention, { type GithubUser, getUserName } from "@/components/user/UserMention" -import { get } from "@/lib/api/request" -import type { User } from "@/lib/api/types" +import GhostButton from "@/components/GhostButton"; +import UserAvatar from "@/components/user/UserAvatar"; +import UserMention, { + type GithubUser, + getUserName, +} from "@/components/user/UserMention"; +import { get } from "@/lib/api/request"; +import type { User } from "@/lib/api/types"; interface GitHubContributor { - login: string - contributions: number + login: string; + contributions: number; } -export type Contributor = User | GithubUser +export type Contributor = User | GithubUser; /** Gets the list of contributor usernames for the repo from GitHub. */ export async function getContributorUsernames(): Promise { - const req = await fetch("https://api.github.com/repos/decompme/decomp.me/contributors?page_size=100", { - cache: "force-cache", - }) + const req = await fetch( + "https://api.github.com/repos/decompme/decomp.me/contributors?page_size=100", + { + cache: "force-cache", + }, + ); if (!req.ok) { - console.warn("failed to fetch contributors:", await req.text()) - return ["ethteck", "nanaian"] + console.warn("failed to fetch contributors:", await req.text()); + return ["ethteck", "nanaian"]; } - const contributors = await req.json() as GitHubContributor[] - contributors.sort((a, b) => b.contributions - a.contributions) - return contributors.map((contributor) => contributor.login) + const contributors = (await req.json()) as GitHubContributor[]; + contributors.sort((a, b) => b.contributions - a.contributions); + return contributors.map((contributor) => contributor.login); } -export async function usernameToContributor(username: string): Promise { +export async function usernameToContributor( + username: string, +): Promise { try { // Try and get decomp.me information if they have an account - const user: User = await get(`/users/${username}`) - return user + const user: User = await get(`/users/${username}`); + return user; } catch (error) { // Fall back to GitHub information - return { login: username } + return { login: username }; } } export function ContributorItem({ contributor }: { contributor: Contributor }) { - return
  • - {!("login" in contributor) && } - -
  • + return ( +
  • + {!("login" in contributor) && ( + + )} + +
  • + ); } -export default function ContributorsList({ contributors }: { contributors: Contributor[] }) { +export default function ContributorsList({ + contributors, +}: { contributors: Contributor[] }) { if (!contributors.length) { - return null + return null; } - return
    -
    -

    - Contributors -

    - - View on GitHub - + return ( +
    +
    +

    + Contributors +

    + + View on GitHub + +
    +
      + {contributors.map((contributor) => ( + + ))} +
    -
      - {contributors.map(contributor => )} -
    -
    + ); } diff --git a/frontend/src/app/(navfooter)/credits/LinkList.tsx b/frontend/src/app/(navfooter)/credits/LinkList.tsx index 1f5261ff7..e40135946 100644 --- a/frontend/src/app/(navfooter)/credits/LinkList.tsx +++ b/frontend/src/app/(navfooter)/credits/LinkList.tsx @@ -1,17 +1,19 @@ -import GhostButton from "@/components/GhostButton" +import GhostButton from "@/components/GhostButton"; export type Props = { - links: { [key: string]: string } -} + links: { [key: string]: string }; +}; export default function LinkList({ links }: Props) { - return
      - {Object.entries(links).map(([name, url]) => { - return
    • - - {name} - -
    • - })} -
    + return ( +
      + {Object.entries(links).map(([name, url]) => { + return ( +
    • + {name} +
    • + ); + })} +
    + ); } diff --git a/frontend/src/app/(navfooter)/credits/page.tsx b/frontend/src/app/(navfooter)/credits/page.tsx index 3f27e335f..676b65814 100644 --- a/frontend/src/app/(navfooter)/credits/page.tsx +++ b/frontend/src/app/(navfooter)/credits/page.tsx @@ -1,99 +1,122 @@ -import UserMention from "@/components/user/UserMention" -import { get } from "@/lib/api/request" -import type { User } from "@/lib/api/types" +import UserMention from "@/components/user/UserMention"; +import { get } from "@/lib/api/request"; +import type { User } from "@/lib/api/types"; -import ContributorsList, { getContributorUsernames, usernameToContributor } from "./ContributorsList" -import LinkList from "./LinkList" +import ContributorsList, { + getContributorUsernames, + usernameToContributor, +} from "./ContributorsList"; +import LinkList from "./LinkList"; -const MAINTAINER_USERNAMES = ["ethteck", "bates64"] +const MAINTAINER_USERNAMES = ["ethteck", "bates64"]; const OTHER_PROJECTS = { "asm-differ": "https://github.com/simonlindholm/asm-differ", - "m2c": "https://github.com/matt-kempster/m2c", - "psyq-obj-parser": "https://github.com/grumpycoders/pcsx-redux/tree/main/tools/psyq-obj-parser", - "Django": "https://www.djangoproject.com/", + m2c: "https://github.com/matt-kempster/m2c", + "psyq-obj-parser": + "https://github.com/grumpycoders/pcsx-redux/tree/main/tools/psyq-obj-parser", + Django: "https://www.djangoproject.com/", "Django REST Framework": "https://www.django-rest-framework.org/", "Next.js": "https://nextjs.org/", - "React": "https://reactjs.org/", + React: "https://reactjs.org/", "Tailwind CSS": "https://tailwindcss.com/", - "SWR": "https://swr.vercel.app/", -} + SWR: "https://swr.vercel.app/", +}; const ICON_SOURCES = { - "Octicons": "https://primer.style/octicons/", + Octicons: "https://primer.style/octicons/", "file-icons/icons": "https://github.com/file-icons/icons", "coreui/coreui-icons": "https://github.com/coreui/coreui-icons", - "New Fontendo 23DSi Lite XL": "https://www.deviantart.com/maxigamer/art/FONT-New-Fontendo-23DSi-Lite-XL-DOWNLOAD-ZIP-552834059", - "GBA SVG by Andrew Vester from NounProject.com": "https://thenounproject.com/icon/gameboy-advanced-752507/", - "Happy Mac by NiloGlock": "https://commons.wikimedia.org/wiki/File:Happy_Mac.svg", - "Tiger-like-x by Althepal": "https://commons.wikimedia.org/wiki/File:Tiger-like-x.svg", - "Saturn by JustDanPatrick": "https://upload.wikimedia.org/wikipedia/commons/archive/7/78/20220518145749%21Sega_Saturn_Black_Logo.svg", - "Dreamcast by Sega": "https://en.wikipedia.org/wiki/File:Dreamcast_logo.svg", - "MS-DOS by Microsoft": "https://commons.wikimedia.org/wiki/File:Msdos-icon.svg", - "Windows 9x by Microsoft": "https://commons.wikimedia.org/wiki/File:Windows_Logo_(1992-2001).svg", + "New Fontendo 23DSi Lite XL": + "https://www.deviantart.com/maxigamer/art/FONT-New-Fontendo-23DSi-Lite-XL-DOWNLOAD-ZIP-552834059", + "GBA SVG by Andrew Vester from NounProject.com": + "https://thenounproject.com/icon/gameboy-advanced-752507/", + "Happy Mac by NiloGlock": + "https://commons.wikimedia.org/wiki/File:Happy_Mac.svg", + "Tiger-like-x by Althepal": + "https://commons.wikimedia.org/wiki/File:Tiger-like-x.svg", + "Saturn by JustDanPatrick": + "https://upload.wikimedia.org/wikipedia/commons/archive/7/78/20220518145749%21Sega_Saturn_Black_Logo.svg", + "Dreamcast by Sega": + "https://en.wikipedia.org/wiki/File:Dreamcast_logo.svg", + "MS-DOS by Microsoft": + "https://commons.wikimedia.org/wiki/File:Msdos-icon.svg", + "Windows 9x by Microsoft": + "https://commons.wikimedia.org/wiki/File:Windows_Logo_(1992-2001).svg", "PerSPire Font by Sean Liew": "https://www.fontspace.com/sean-liew", -} +}; -type Contributor = { - type: "decompme" - user: User -} | { - type: "github" - user: { login: string } -} +type Contributor = + | { + type: "decompme"; + user: User; + } + | { + type: "github"; + user: { login: string }; + }; async function getContributor(username: string): Promise { try { // Try and get decomp.me information if they have an account - const user: User = await get(`/users/${username}`) + const user: User = await get(`/users/${username}`); return { type: "decompme", user, - } + }; } catch (error) { // Fall back to GitHub information // No need to ask their API for data since we just need the username return { type: "github", user: { login: username }, - } + }; } } function Contributor({ contributor }: { contributor: Contributor }) { - return + return ; } export const metadata = { title: "Credits", -} +}; export default async function Page() { - const maintainers = await Promise.all(MAINTAINER_USERNAMES.map(getContributor)) - const contributors = await getContributorUsernames().then(usernames => Promise.all(usernames.map(usernameToContributor))) + const maintainers = await Promise.all( + MAINTAINER_USERNAMES.map(getContributor), + ); + const contributors = await getContributorUsernames().then((usernames) => + Promise.all(usernames.map(usernameToContributor)), + ); - return
    -
    -

    - Credits -

    -

    - decomp.me is maintained by and . -

    -
    - -
    -
    -

    - Acknowledgements -

    -

    - decomp.me is built on top of many other open source projects, including: -

    - -

    - We also use icons from the following sources: + return ( +

    +
    +

    + Credits +

    +

    + decomp.me is maintained by{" "} + and{" "} + .

    - +
    + +
    +
    +

    + Acknowledgements +

    +

    + decomp.me is built on top of many other open source + projects, including: +

    + +

    + We also use icons from the following sources: +

    + +
    -
    -
    + + ); } diff --git a/frontend/src/app/(navfooter)/error.tsx b/frontend/src/app/(navfooter)/error.tsx index 2cfae610a..d3be84292 100644 --- a/frontend/src/app/(navfooter)/error.tsx +++ b/frontend/src/app/(navfooter)/error.tsx @@ -1,7 +1,7 @@ // Rexport app error page to include the layout -"use client" +"use client"; -import ErrorPage from "../error" +import ErrorPage from "../error"; -export default ErrorPage +export default ErrorPage; diff --git a/frontend/src/app/(navfooter)/faq/page.tsx b/frontend/src/app/(navfooter)/faq/page.tsx index 65661c48e..e0f86eea7 100644 --- a/frontend/src/app/(navfooter)/faq/page.tsx +++ b/frontend/src/app/(navfooter)/faq/page.tsx @@ -1,128 +1,205 @@ -import type { ReactNode } from "react" +import type { ReactNode } from "react"; -import Link from "next/link" +import Link from "next/link"; -import Frog from "@/components/Nav/frog.svg" +import Frog from "@/components/Nav/frog.svg"; -const subtitle = "mt-8 text-xl font-semibold tracking-tight text-gray-11" +const subtitle = "mt-8 text-xl font-semibold tracking-tight text-gray-11"; -function FaqLink({ children, href }: { children: ReactNode, href?: string }) { - return - {children} - +function FaqLink({ children, href }: { children: ReactNode; href?: string }) { + return ( + + {children} + + ); } function DiscordLink() { - return Discord Server + return ( + Discord Server + ); } export const metadata = { title: "Frequently Asked Questions", -} +}; export default function Page() { - return
    -

    - Frequently Asked Questions -

    - -

    What is decomp.me?

    -

    - decomp.me is an interactive web-based platform where users can collaboratively decompile assembly code snippets by writing matching code. -

    -

    - It is an open-source project run by volunteers in their free time. -

    - -

    What do you mean by "matching"?

    -

    - decomp.me is designed for users who are working on matching decompilation projects, where the goal is to produce high-level code like C or C++ that perfectly replicates the original assembly upon compilation. -

    -

    - This is a time- and labor-intensive process. To produce matching assembly, one usually needs the original compiler, assembler, and flags that were used to produce the original binary. - Most importantly, the code has to be written in such a way that the compiler will generate assembly that is identical to what is being compared against. - Writing matching code is a skill that takes time to learn, but it can be very rewarding and addictive. -

    - -

    What's a scratch?

    -

    - A scratch is a workspace for exploring a compiler's codegen, similar to Compiler Explorer. - A scratch consists of the target assembly, input source code, and input context code, as well as additional settings and metadata. - Most scratches contain a single function - i.e. the function that you are trying to match. - Each scratch has a unique link that can be shared with others. Scratches have a "family" of forks, which are copies of the original scratch. -

    - -

    What's the context for?

    -

    - The context is a separate section of code that usually contains definitions and declarations, such as structs, externs, function declarations, and things of that nature. - The context is passed to the compiler along with the code, so it's a good way to organize a scratch's functional code from its definitions and macros. -

    - -

    - Context is also given to the decompiler to assist with typing information and more accurate decompilation. -

    - -

    How does decomp.me work?

    -

    - The code from your scratch is submitted to the decomp.me server where it is compiled, run through objdump, and then compared against the target assembly. - As you modify your code in the editor, the changes will be sent to the backend and compiled, so you'll see the results of your change in near real-time. - The similarity between the compiled code's assembly and the target assembly is represented by a score, which is displayed in the editor. -

    - -

    - The score is calculated by comparing the assembly instructions in the compiled code to the target assembly, and a penalty of different size is applied based on the kind of difference present among assembly instructions. - The lower the score, the better! -

    - -

    Where do I start?

    -

    - Currently, this website is meant to be used as a supplementary tool along with an existing decompilation project. - Eventually, we hope to make the website a little more friendly to complete newcomers who aren't involved with any specific project. - In the meantime, feel free to explore recent scratches and get a feel for how matching decomp works! -

    - -

    Someone sent me a scratch. What do I do?

    -

    - Any scratch on the site can be played with. If you save a scratch that you don't own, your scratch will become a "fork" of the original. - If you match the scratch, the original scratch will display a banner to notify visitors that the code is matched. -

    -

    - If you want to start your own scratch, you will need the assembly code for the function you are targeting in GNU assembly format. -

    - -

    Can you help me match a scratch?

    -

    - You are welcome to ask for help in the #general-decompilation channel of our . -

    - -

    Can you add a preset for a game I'm working on?

    -

    - Absolutely we can, either raise a GitHub Issue or drop us a message in our . -

    - -

    Can you add a compiler for a game I'm working on?

    -

    - This is something that you are able to do yourself. - The compilers used by decomp.me can be found in our compilers repository. - Once the compiler has been added to that repo, it is very simple to add it to decomp.me, see this PR for an example. -

    - -

    Can you add "X" platform, e.g. PlayStation 3?

    -

    - The platforms that decomp.me supports are driven by the support for those platforms in the underlying tools that make up decomp.me. - If these tools support the architecture for the new platform, and you have the compiler available, it is a straightforward process to add it to decomp.me. -

    - -

    How do I report a bug?

    -

    - If you come across a bug, please reach out to us on our and/or raise a GitHub Issue containing the steps necessary to replicate the bug. - We will gladly accept bug-squashing PRs if you are able to fix the issue yourself! -

    - -

    Why frog?

    -

    - -

    - -
    + return ( +
    +

    + Frequently Asked Questions +

    + +

    What is decomp.me?

    +

    + decomp.me is an interactive web-based platform where users can + collaboratively decompile assembly code snippets by writing + matching code. +

    +

    + It is an{" "} + + open-source project + {" "} + run by volunteers in their free time. +

    + +

    What do you mean by "matching"?

    +

    + decomp.me is designed for users who are working on matching + decompilation projects, where the goal is to produce high-level + code like C or C++ that perfectly replicates the original + assembly upon compilation. +

    +

    + This is a time- and labor-intensive process. To produce matching + assembly, one usually needs the original compiler, assembler, + and flags that were used to produce the original binary. Most + importantly, the code has to be written in such a way that the + compiler will generate assembly that is identical to what is + being compared against. Writing matching code is a skill that + takes time to learn, but it can be very rewarding and addictive. +

    + +

    What's a scratch?

    +

    + A scratch is a workspace for exploring a compiler's codegen, + similar to{" "} + Compiler Explorer + . A scratch consists of the target assembly, input source code, + and input context code, as well as additional settings and + metadata. Most scratches contain a single function - i.e. the + function that you are trying to match. Each scratch has a unique + link that can be shared with others. Scratches have a "family" + of forks, which are copies of the original scratch. +

    + +

    What's the context for?

    +

    + The context is a separate section of code that usually contains + definitions and declarations, such as structs, externs, function + declarations, and things of that nature. The context is passed + to the compiler along with the code, so it's a good way to + organize a scratch's functional code from its definitions and + macros. +

    + +

    + Context is also given to the decompiler to assist with typing + information and more accurate decompilation. +

    + +

    How does decomp.me work?

    +

    + The code from your scratch is submitted to the decomp.me server + where it is compiled, run through objdump, and then compared + against the target assembly. As you modify your code in the + editor, the changes will be sent to the backend and compiled, so + you'll see the results of your change in near real-time. The + similarity between the compiled code's assembly and the target + assembly is represented by a score, which is displayed in the + editor. +

    + +

    + The score is calculated by comparing the assembly instructions + in the compiled code to the target assembly, and a penalty of + different size is applied based on the kind of difference + present among assembly instructions. The lower the score, the + better! +

    + +

    Where do I start?

    +

    + Currently, this website is meant to be used as a supplementary + tool along with an existing decompilation project. Eventually, + we hope to make the website a little more friendly to complete + newcomers who aren't involved with any specific project. In the + meantime, feel free to explore recent scratches and get a feel + for how matching decomp works! +

    + +

    + Someone sent me a scratch. What do I do? +

    +

    + Any scratch on the site can be played with. If you save a + scratch that you don't own, your scratch will become a "fork" of + the original. If you match the scratch, the original scratch + will display a banner to notify visitors that the code is + matched. +

    +

    + If you want to start your own scratch, you will need the + assembly code for the function you are targeting in GNU assembly + format. +

    + +

    Can you help me match a scratch?

    +

    + You are welcome to ask for help in the #general-decompilation + channel of our . +

    + +

    + Can you add a preset for a game I'm working on? +

    +

    + Absolutely we can, either raise a{" "} + + GitHub Issue + {" "} + or drop us a message in our . +

    + +

    + Can you add a compiler for a game I'm working on? +

    +

    + This is something that you are able to do yourself. The + compilers used by decomp.me can be found in our{" "} + + compilers repository + + . Once the compiler has been added to that repo, it is very + simple to add it to decomp.me, see{" "} + + this PR + {" "} + for an example. +

    + +

    + Can you add "X" platform, e.g. PlayStation 3? +

    +

    + The platforms that decomp.me supports are driven by the support + for those platforms in the underlying tools that make up + decomp.me. If these tools support the architecture for the new + platform, and you have the compiler available, it is a + straightforward process to add it to decomp.me. +

    + +

    How do I report a bug?

    +

    + If you come across a bug, please reach out to us on our{" "} + and/or raise a{" "} + + GitHub Issue + {" "} + containing the steps necessary to replicate the bug. We will + gladly accept bug-squashing PRs if you are able to fix the issue + yourself! +

    + +

    Why frog?

    +

    + +

    +
    + ); } diff --git a/frontend/src/app/(navfooter)/layout.tsx b/frontend/src/app/(navfooter)/layout.tsx index 5a20a3bf4..d3ec2d5ae 100644 --- a/frontend/src/app/(navfooter)/layout.tsx +++ b/frontend/src/app/(navfooter)/layout.tsx @@ -1,19 +1,21 @@ -import ErrorBoundary from "@/components/ErrorBoundary" -import Footer from "@/components/Footer" -import Nav from "@/components/Nav" +import ErrorBoundary from "@/components/ErrorBoundary"; +import Footer from "@/components/Footer"; +import Nav from "@/components/Nav"; export default function RootLayout({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { - return <> - -
    + ); } diff --git a/frontend/src/app/(navfooter)/new/description.ts b/frontend/src/app/(navfooter)/new/description.ts index 86f2f20b6..1038c0744 100644 --- a/frontend/src/app/(navfooter)/new/description.ts +++ b/frontend/src/app/(navfooter)/new/description.ts @@ -1,3 +1,4 @@ -const DESCRIPTION = "A scratch is a playground where you can work on matching a given target function using any compiler options you like." +const DESCRIPTION = + "A scratch is a playground where you can work on matching a given target function using any compiler options you like."; -export default DESCRIPTION +export default DESCRIPTION; diff --git a/frontend/src/app/(navfooter)/new/layout.tsx b/frontend/src/app/(navfooter)/new/layout.tsx index 4eba031e4..7bf201ea0 100644 --- a/frontend/src/app/(navfooter)/new/layout.tsx +++ b/frontend/src/app/(navfooter)/new/layout.tsx @@ -1,7 +1,9 @@ -import type { ReactNode } from "react" +import type { ReactNode } from "react"; export default function Layout({ children }: { children: ReactNode }) { - return
    - {children} -
    + return ( +
    + {children} +
    + ); } diff --git a/frontend/src/app/(navfooter)/new/page.tsx b/frontend/src/app/(navfooter)/new/page.tsx index 09df93979..8c1c4d7af 100644 --- a/frontend/src/app/(navfooter)/new/page.tsx +++ b/frontend/src/app/(navfooter)/new/page.tsx @@ -1,18 +1,22 @@ -import { get } from "@/lib/api/request" +import { get } from "@/lib/api/request"; -import DESCRIPTION from "./description" -import NewScratchForm from "./NewScratchForm" +import DESCRIPTION from "./description"; +import NewScratchForm from "./NewScratchForm"; export const metadata = { title: "New scratch", -} +}; export default async function NewScratchPage() { - const compilers = await get("/compiler") + const compilers = await get("/compiler"); - return
    -

    Start a new scratch

    -

    {DESCRIPTION}

    - -
    + return ( +
    +

    + Start a new scratch +

    +

    {DESCRIPTION}

    + +
    + ); } diff --git a/frontend/src/app/(navfooter)/page.tsx b/frontend/src/app/(navfooter)/page.tsx index 136ff7fb0..fab4e4e37 100644 --- a/frontend/src/app/(navfooter)/page.tsx +++ b/frontend/src/app/(navfooter)/page.tsx @@ -1,14 +1,14 @@ -import type { Metadata } from "next" +import type { Metadata } from "next"; -import ScratchList, { SingleLineScratchItem } from "@/components/ScratchList" -import YourScratchList from "@/components/YourScratchList" +import ScratchList, { SingleLineScratchItem } from "@/components/ScratchList"; +import YourScratchList from "@/components/YourScratchList"; -import WelcomeInfo from "./WelcomeInfo" +import WelcomeInfo from "./WelcomeInfo"; export async function generateMetadata(): Promise { - const title = "decomp.me" + const title = "decomp.me"; - const description = "A collaborative decompilation platform." + const description = "A collaborative decompilation platform."; return { openGraph: { @@ -24,25 +24,25 @@ export async function generateMetadata(): Promise { }, ], }, - } + }; } export default function Page() { - return
    -
    - -
    -
    -
    -

    Your scratches

    - -
    -
    -

    Recent activity

    - -
    -
    -
    + return ( +
    +
    + +
    +
    +
    +

    Your scratches

    + +
    +
    +

    Recent activity

    + +
    +
    +
    + ); } diff --git a/frontend/src/app/(navfooter)/platform/[id]/page.tsx b/frontend/src/app/(navfooter)/platform/[id]/page.tsx index 7efb05748..7f8945057 100644 --- a/frontend/src/app/(navfooter)/platform/[id]/page.tsx +++ b/frontend/src/app/(navfooter)/platform/[id]/page.tsx @@ -1,30 +1,35 @@ -import type { Metadata } from "next" +import type { Metadata } from "next"; -import { notFound } from "next/navigation" +import { notFound } from "next/navigation"; -import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon" -import ScratchList, { ScratchItemPlatformList } from "@/components/ScratchList" -import { get } from "@/lib/api/request" -import type { PlatformMetadata } from "@/lib/api/types" +import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon"; +import ScratchList, { ScratchItemPlatformList } from "@/components/ScratchList"; +import { get } from "@/lib/api/request"; +import type { PlatformMetadata } from "@/lib/api/types"; -export async function generateMetadata({ params }: { params: { id: number } }):Promise { - let platform: PlatformMetadata +export async function generateMetadata({ + params, +}: { params: { id: number } }): Promise { + let platform: PlatformMetadata; try { - platform = await get(`/platform/${params.id}`) + platform = await get(`/platform/${params.id}`); } catch (error) { - console.error(error) + console.error(error); } if (!platform) { - return notFound() + return notFound(); } - let description = "There " - description += platform.num_scratches === 1 ? "is " : "are " - description += platform.num_scratches === 0 ? "currently no " : `${platform.num_scratches.toLocaleString("en-US")} ` - description += platform.num_scratches === 1 ? "scratch " : "scratches " - description += "for this platform." + let description = "There "; + description += platform.num_scratches === 1 ? "is " : "are "; + description += + platform.num_scratches === 0 + ? "currently no " + : `${platform.num_scratches.toLocaleString("en-US")} `; + description += platform.num_scratches === 1 ? "scratch " : "scratches "; + description += "for this platform."; return { title: platform.name, @@ -32,37 +37,37 @@ export async function generateMetadata({ params }: { params: { id: number } }):P title: platform.name, description: description, }, - } + }; } export default async function Page({ params }: { params: { id: number } }) { - let platform: PlatformMetadata + let platform: PlatformMetadata; try { - platform = await get(`/platform/${params.id}`) + platform = await get(`/platform/${params.id}`); } catch (error) { - console.error(error) + console.error(error); } if (!platform) { - return notFound() + return notFound(); } - return
    -
    - -

    - {platform.name} -

    -
    -

    {platform.description}

    + return ( +
    +
    + +

    {platform.name}

    +
    +

    {platform.description}

    -
    - -
    -
    +
    + +
    +
    + ); } diff --git a/frontend/src/app/(navfooter)/preset/[id]/page.tsx b/frontend/src/app/(navfooter)/preset/[id]/page.tsx index de7ff7519..10f968a59 100644 --- a/frontend/src/app/(navfooter)/preset/[id]/page.tsx +++ b/frontend/src/app/(navfooter)/preset/[id]/page.tsx @@ -1,31 +1,36 @@ -import type { Metadata } from "next" +import type { Metadata } from "next"; -import { notFound } from "next/navigation" +import { notFound } from "next/navigation"; -import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon" -import ScratchList, { ScratchItemPresetList } from "@/components/ScratchList" -import { get } from "@/lib/api/request" -import type { Preset } from "@/lib/api/types" -import getTranslation from "@/lib/i18n/translate" +import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon"; +import ScratchList, { ScratchItemPresetList } from "@/components/ScratchList"; +import { get } from "@/lib/api/request"; +import type { Preset } from "@/lib/api/types"; +import getTranslation from "@/lib/i18n/translate"; -export async function generateMetadata({ params }: { params: { id: number } }): Promise { - let preset: Preset +export async function generateMetadata({ + params, +}: { params: { id: number } }): Promise { + let preset: Preset; try { - preset = await get(`/preset/${params.id}`) + preset = await get(`/preset/${params.id}`); } catch (error) { - console.error(error) + console.error(error); } if (!preset) { - return notFound() + return notFound(); } - let description = "There " - description += preset.num_scratches === 1 ? "is " : "are " - description += preset.num_scratches === 0 ? "currently no " : `${preset.num_scratches.toLocaleString("en-US")} ` - description += preset.num_scratches === 1 ? "scratch " : "scratches " - description += "that use this preset." + let description = "There "; + description += preset.num_scratches === 1 ? "is " : "are "; + description += + preset.num_scratches === 0 + ? "currently no " + : `${preset.num_scratches.toLocaleString("en-US")} `; + description += preset.num_scratches === 1 ? "scratch " : "scratches "; + description += "that use this preset."; return { title: preset.name, @@ -33,41 +38,41 @@ export async function generateMetadata({ params }: { params: { id: number } }): title: preset.name, description: description, }, - } + }; } export default async function Page({ params }: { params: { id: number } }) { - const compilersTranslation = getTranslation("compilers") + const compilersTranslation = getTranslation("compilers"); - let preset: Preset + let preset: Preset; try { - preset = await get(`/preset/${params.id}`) + preset = await get(`/preset/${params.id}`); } catch (error) { - console.error(error) + console.error(error); } if (!preset) { - return notFound() + return notFound(); } - const compilerName = compilersTranslation.t(preset.compiler) + const compilerName = compilersTranslation.t(preset.compiler); - return
    -
    - -

    - {preset.name} -

    -
    -

    {compilerName}

    + return ( +
    +
    + +

    {preset.name}

    +
    +

    {compilerName}

    -
    - -
    -
    +
    + +
    +
    + ); } diff --git a/frontend/src/app/(navfooter)/preset/page.tsx b/frontend/src/app/(navfooter)/preset/page.tsx index f6dab82e7..3a1df05fd 100644 --- a/frontend/src/app/(navfooter)/preset/page.tsx +++ b/frontend/src/app/(navfooter)/preset/page.tsx @@ -1,12 +1,12 @@ -import { Presets } from "@/app/(navfooter)/preset/presets" -import { get } from "@/lib/api/request" +import { Presets } from "@/app/(navfooter)/preset/presets"; +import { get } from "@/lib/api/request"; export default async function Page() { - const compilers = await get("/compiler") + const compilers = await get("/compiler"); return (
    - +
    - ) + ); } diff --git a/frontend/src/app/(navfooter)/preset/presets.tsx b/frontend/src/app/(navfooter)/preset/presets.tsx index d93d5e096..4c518cc09 100644 --- a/frontend/src/app/(navfooter)/preset/presets.tsx +++ b/frontend/src/app/(navfooter)/preset/presets.tsx @@ -1,36 +1,41 @@ -"use client" +"use client"; -import { useState } from "react" +import { useState } from "react"; -import PlatformSelect from "@/components/PlatformSelect" -import { PresetList } from "@/components/PresetList" -import type * as api from "@/lib/api" +import PlatformSelect from "@/components/PlatformSelect"; +import { PresetList } from "@/components/PresetList"; +import type * as api from "@/lib/api"; -export function Presets({ serverCompilers }: { +export function Presets({ + serverCompilers, +}: { serverCompilers: { platforms: { - [id: string]: api.Platform - } + [id: string]: api.Platform; + }; compilers: { - [id: string]: api.Compiler - } - } + [id: string]: api.Compiler; + }; + }; }) { + const platforms = Object.keys(serverCompilers.platforms); - const platforms = Object.keys(serverCompilers.platforms) - - const [platform, setPlatform] = useState(platforms.length > 0 ? platforms[0] : "") + const [platform, setPlatform] = useState( + platforms.length > 0 ? platforms[0] : "", + ); return (
    -

    Platforms

    +

    + Platforms +

    Presets

    - +
    - ) + ); } diff --git a/frontend/src/app/(navfooter)/privacy/page.tsx b/frontend/src/app/(navfooter)/privacy/page.tsx index c5eb51897..bc31ae628 100644 --- a/frontend/src/app/(navfooter)/privacy/page.tsx +++ b/frontend/src/app/(navfooter)/privacy/page.tsx @@ -1,112 +1,159 @@ -import Link from "next/link" +import Link from "next/link"; -const subtitle = "mt-8 text-xl font-semibold tracking-tight text-gray-11" -const link = "text-blue-11 hover:underline active:translate-y-px" +const subtitle = "mt-8 text-xl font-semibold tracking-tight text-gray-11"; +const link = "text-blue-11 hover:underline active:translate-y-px"; export const metadata = { title: "Privacy policy", -} +}; export default function Page() { - return
    -

    - Privacy policy -

    + return ( +
    +

    + Privacy policy +

    -

    - Last updated January 13th 2022 -

    +

    + Last updated January 13th 2022 +

    -

    - For the purposes of this document, "We", "our", and "decomp.me" refers to this - website, its API, and its administrators. - "You" and "user" refers to any person or robot visiting this website. -

    +

    + For the purposes of this document, "We", "our", and "decomp.me" + refers to this website, its API, and its administrators. "You" + and "user" refers to any person or robot visiting this website. +

    -

    Your privacy

    -

    - We care and respect your right to privacy, and only store data we believe we have - legitimate uses for. We have made every effort to ensure that we are compliant with - privacy regulations such as GDPR, CCPA, and PECR. -

    +

    Your privacy

    +

    + We care and respect your right to privacy, and only store data + we believe we have legitimate uses for. We have made every + effort to ensure that we are compliant with privacy regulations + such as GDPR, CCPA, and PECR. +

    -

    Types of data we collect

    -

    - Logging: decomp.me stores logs when users make requests to - decomp.me and its associated API. Data logs are restricted to IP address, - request path, and time/date. All logs older than 7 days are automatically - deleted in the interests of data minimization. - We will only use logs data in exceptional circumstances which we believe to - be reasonable, such as to defend against attacks against our servers. - Logging IP addresses for the legitimate purpose of security is a widespread practice - and does not conflict with privacy regulations. -

    -

    - Analytics: we use the open source Plausible Analytics software routed through our stats - subdomain to count website visits etc. - All analytics data collected is publicly available on stats.decomp.me. - All site measurement is carried out absolutely anonymously and in aggregate only. - Analytics data collected is limited to: -

    -
      -
    • Page URL
    • -
    • HTTP Referer
    • -
    • Browser and operating system (using User-Agent HTTP header, which is discarded)
    • -
    • Device type (using screen width, which is discarded)
    • -
    • Country, region, city (using IP address, which is then discarded)
    • -
    • Actions taken on the site, such as compiling or saving a scratch
    • -
    -

    - For more information about analytics data, see the Plausible Data Policy. - Please note that decomp.me servers, not Plausible, store and process our analytics data. -

    -

    - Voluntarily-submitted information: decomp.me collects and retains information - voluntarily submitted to us. For logged-in users, this includes basic GitHub profile - information such as name, email, and avatar. For all users, data submitted on the - new scratch page and saved in the scratch editor will be stored and linked to your - session. -

    -

    - Cookies: decomp.me uses a single persistent authentication cookie used to link - voluntarily-submitted information to your session on our site. If you are logged in, - this cookie will link your session to your account on decomp.me. We do not show any - 'cookie banners' or 'privacy popups' on decomp.me because we do not use any third-party - or analytics cookies. -

    +

    Types of data we collect

    +

    + Logging: decomp.me stores logs when users make requests + to decomp.me and its associated API. Data logs are restricted to + IP address, request path, and time/date. All logs older than 7 + days are automatically deleted in the interests of data + minimization. We will only use logs data in exceptional + circumstances which we believe to be reasonable, such as to + defend against attacks against our servers. Logging IP addresses + for the legitimate purpose of security is a widespread practice + and does not conflict with privacy regulations. +

    +

    + Analytics: we use the open source Plausible Analytics + software routed through our stats subdomain to count website + visits etc. All analytics data collected is publicly available + on{" "} + + stats.decomp.me + + . All site measurement is carried out absolutely anonymously and + in aggregate only. Analytics data collected is limited to: +

    +
      +
    • Page URL
    • +
    • HTTP Referer
    • +
    • + Browser and operating system (using User-Agent HTTP header, + which is discarded) +
    • +
    • + Device type (using screen width, which is discarded) +
    • +
    • + Country, region, city (using IP address, which is then + discarded) +
    • +
    • + Actions taken on the site, such as compiling or saving a + scratch +
    • +
    +

    + For more information about analytics data, see the{" "} + + Plausible Data Policy + + . Please note that decomp.me servers, not Plausible, store and + process our analytics data. +

    +

    + Voluntarily-submitted information: decomp.me collects and + retains information voluntarily submitted to us. For logged-in + users, this includes basic GitHub profile information such as + name, email, and avatar. For all users, data submitted on the + new scratch page and saved in the scratch editor will be stored + and linked to your session. +

    +

    + Cookies: decomp.me uses a single persistent + authentication cookie used to link voluntarily-submitted + information to your session on our site. If you are logged in, + this cookie will link your session to your account on decomp.me. + We do not show any 'cookie banners' or 'privacy popups' on + decomp.me because we do not use any third-party or analytics + cookies. +

    -

    How data is stored and used

    -

    - decomp.me does not sell, rent, or mine user information under any circumstances. - decomp.me's servers are located in Finland. which means that we will - transfer, process, and store your information there. In very extreme cases, such as if - required by police or other government agencies, data may be disclosed. -

    -

    - Analytics data is used to prioritise what site features and fixes should be worked - on and to let us determine features which are popular or unpopular. -

    -

    - Voluntarily-submitted information is used to provide vital site features such - as user profile pages and the scratch editor. We also reserve the right to use - any and all voluntarily-submited information for improving existing decompilation - tools and developing new ones. This will not involve sharing information with third parties. -

    -

    - We make every effort to keep your data secure. In the case of a breach, we will - notify you and take appropriate action, such as revoking GitHub OAuth tokens. - Please note that our servers never receive or store user passwords. -

    +

    How data is stored and used

    +

    + decomp.me does not sell, rent, or mine user information under + any circumstances. decomp.me's servers are located in Finland. + which means that we will transfer, process, and store your + information there. In very extreme cases, such as if required by + police or other government agencies, data may be disclosed. +

    +

    + Analytics data is used to prioritise what site features and + fixes should be worked on and to let us determine features which + are popular or unpopular. +

    +

    + Voluntarily-submitted information is used to provide vital site + features such as user profile pages and the scratch editor. We + also reserve the right to use any and all voluntarily-submited + information for improving existing decompilation tools and + developing new ones. This will not involve sharing information + with third parties. +

    +

    + We make every effort to keep your data secure. In the case of a + breach, we will notify you and take appropriate action, such as + revoking GitHub OAuth tokens. Please note that our servers never + receive or store user passwords. +

    -

    How to request your data or delete it

    -

    - If you want us to delete some or all data linked to you, please contact us via our Discord server or GitHub Issues. - You may also want to disassociate your GitHub account with decomp.me. -

    -

    - You may contact us through the same channels linked above if you would like to request - a copy of all data linked to you. Similarly, please contact us if you have any questions - or concerns regarding this document. -

    -
    +

    How to request your data or delete it

    +

    + If you want us to delete some or all data linked to you, please + contact us via{" "} + + our Discord server + {" "} + or{" "} + + GitHub Issues + + . You may also want to{" "} + + disassociate your GitHub account with decomp.me + + . +

    +

    + You may contact us through the same channels linked above if you + would like to request a copy of all data linked to you. + Similarly, please contact us if you have any questions or + concerns regarding this document. +

    +
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/Checkbox.tsx b/frontend/src/app/(navfooter)/settings/Checkbox.tsx index 324599990..adc28a7d3 100644 --- a/frontend/src/app/(navfooter)/settings/Checkbox.tsx +++ b/frontend/src/app/(navfooter)/settings/Checkbox.tsx @@ -1,32 +1,42 @@ -import { type ReactNode, useId } from "react" +import { type ReactNode, useId } from "react"; export type Props = { - checked: boolean - onChange: (checked: boolean) => void + checked: boolean; + onChange: (checked: boolean) => void; - label: ReactNode - description?: ReactNode - children?: ReactNode -} + label: ReactNode; + description?: ReactNode; + children?: ReactNode; +}; -export default function Checkbox({ checked, onChange, label, description, children }: Props) { - const id = useId() +export default function Checkbox({ + checked, + onChange, + label, + description, + children, +}: Props) { + const id = useId(); - return
    -
    - onChange(evt.target.checked)} - /> -
    -
    - - {description &&
    {description}
    } - {children &&
    - {children} -
    } + return ( +
    +
    + onChange(evt.target.checked)} + /> +
    +
    + + {description && ( +
    {description}
    + )} + {children &&
    {children}
    } +
    -
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/NavItem.tsx b/frontend/src/app/(navfooter)/settings/NavItem.tsx index 1eb5d62cd..d1fb49a00 100644 --- a/frontend/src/app/(navfooter)/settings/NavItem.tsx +++ b/frontend/src/app/(navfooter)/settings/NavItem.tsx @@ -1,32 +1,34 @@ -"use client" +"use client"; -import type { ReactNode } from "react" +import type { ReactNode } from "react"; -import { useSelectedLayoutSegment } from "next/navigation" +import { useSelectedLayoutSegment } from "next/navigation"; -import classNames from "classnames" +import classNames from "classnames"; -import GhostButton from "@/components/GhostButton" +import GhostButton from "@/components/GhostButton"; export type Props = { - segment: string - icon: ReactNode - label: ReactNode -} + segment: string; + icon: ReactNode; + label: ReactNode; +}; export default function NavItem({ segment, label, icon }: Props) { - const isSelected = useSelectedLayoutSegment() === segment + const isSelected = useSelectedLayoutSegment() === segment; - return
  • - - {icon} - {label} - -
  • + return ( +
  • + + {icon} + {label} + +
  • + ); } diff --git a/frontend/src/app/(navfooter)/settings/RadioList.tsx b/frontend/src/app/(navfooter)/settings/RadioList.tsx index 92dfb6142..f75795532 100644 --- a/frontend/src/app/(navfooter)/settings/RadioList.tsx +++ b/frontend/src/app/(navfooter)/settings/RadioList.tsx @@ -1,54 +1,76 @@ -import { type ReactNode, useId } from "react" +import { type ReactNode, useId } from "react"; -function RadioButton({ name, value, checked, onChange, option }: { name: string, value: string, checked: boolean, onChange: (value: string) => void, option: Option }) { - const id = useId() +function RadioButton({ + name, + value, + checked, + onChange, + option, +}: { + name: string; + value: string; + checked: boolean; + onChange: (value: string) => void; + option: Option; +}) { + const id = useId(); - return
    -
    - onChange(evt.target.value)} - /> + return ( +
    +
    + onChange(evt.target.value)} + /> +
    +
    + + {option.description && ( +
    + {option.description} +
    + )} + {option.children && ( +
    {option.children}
    + )} +
    -
    - - {option.description &&
    {option.description}
    } - {option.children &&
    - {option.children} -
    } -
    -
    + ); } export type Option = { - label: ReactNode - description?: ReactNode - children?: ReactNode -} + label: ReactNode; + description?: ReactNode; + children?: ReactNode; +}; export type Props = { - value: string - onChange: (value: string) => void - options: { [key: string]: Option } -} + value: string; + onChange: (value: string) => void; + options: { [key: string]: Option }; +}; export default function RadioList({ value, onChange, options }: Props) { - const name = useId() + const name = useId(); - return
    - {Object.keys(options).map(key => - - )} -
    + return ( +
    + {Object.keys(options).map((key) => ( + + ))} +
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/Section.tsx b/frontend/src/app/(navfooter)/settings/Section.tsx index 98c5f7dfa..682488d16 100644 --- a/frontend/src/app/(navfooter)/settings/Section.tsx +++ b/frontend/src/app/(navfooter)/settings/Section.tsx @@ -1,15 +1,17 @@ -import type { ReactNode } from "react" +import type { ReactNode } from "react"; export type Props = { - title: string - children: ReactNode -} + title: string; + children: ReactNode; +}; export default function Section({ title, children }: Props) { - return
    -

    {title}

    -
    - {children} -
    -
    + return ( +
    +

    + {title} +

    +
    {children}
    +
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/SliderField.tsx b/frontend/src/app/(navfooter)/settings/SliderField.tsx index 9a2fd1994..cfd5db1b8 100644 --- a/frontend/src/app/(navfooter)/settings/SliderField.tsx +++ b/frontend/src/app/(navfooter)/settings/SliderField.tsx @@ -1,66 +1,86 @@ -import { useId, type ReactNode } from "react" +import { useId, type ReactNode } from "react"; -import classNames from "classnames" +import classNames from "classnames"; -import NumberInput from "@/components/NumberInput" +import NumberInput from "@/components/NumberInput"; function clamp(value: number, min: number, max: number) { - return Math.min(Math.max(value, min), max) + return Math.min(Math.max(value, min), max); } export type Props = { - value: number - onChange: (value: number) => void - disabled?: boolean + value: number; + onChange: (value: number) => void; + disabled?: boolean; - label: ReactNode - description?: ReactNode - unit?: ReactNode + label: ReactNode; + description?: ReactNode; + unit?: ReactNode; - min: number - max: number - step: number -} + min: number; + max: number; + step: number; +}; -export default function SliderField({ value, onChange, disabled, label, description, unit, min, max, step }: Props) { - const id = useId() +export default function SliderField({ + value, + onChange, + disabled, + label, + description, + unit, + min, + max, + step, +}: Props) { + const id = useId(); - return
    - + return ( +
    + -
    -
    - onChange(clamp(newValue, min, max))} - disabled={disabled} - /> - {unit} -
    +
    +
    + + onChange(clamp(newValue, min, max)) + } + disabled={disabled} + /> + {unit} +
    -
    - {min}{unit} - onChange(clamp(+evt.target.value, min, max))} - disabled={disabled} - className="w-full focus:ring" - /> - {max}{unit} +
    + {min} + {unit} + + onChange(clamp(+evt.target.value, min, max)) + } + disabled={disabled} + className="w-full focus:ring" + /> + {max} + {unit} +
    -
    - {description &&
    {description}
    } -
    + {description && ( +
    {description}
    + )} +
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/TextField.tsx b/frontend/src/app/(navfooter)/settings/TextField.tsx index 84459ba30..452defb5b 100644 --- a/frontend/src/app/(navfooter)/settings/TextField.tsx +++ b/frontend/src/app/(navfooter)/settings/TextField.tsx @@ -1,42 +1,54 @@ -import { useId, type ReactNode, type CSSProperties } from "react" +import { useId, type ReactNode, type CSSProperties } from "react"; -import classNames from "classnames" +import classNames from "classnames"; export type Props = { - value: string - onChange: (value: string) => void - disabled?: boolean + value: string; + onChange: (value: string) => void; + disabled?: boolean; - label: ReactNode - description?: ReactNode + label: ReactNode; + description?: ReactNode; - placeholder?: string + placeholder?: string; - inputStyle?: CSSProperties -} + inputStyle?: CSSProperties; +}; -export default function TextField({ value, onChange, disabled, label, description, placeholder, inputStyle }: Props) { - const id = useId() +export default function TextField({ + value, + onChange, + disabled, + label, + description, + placeholder, + inputStyle, +}: Props) { + const id = useId(); - return
    - - {description &&
    {description}
    } - onChange(evt.target.value)} - disabled={disabled} - placeholder={placeholder} - spellCheck={false} - className="mt-1 block w-full rounded border border-gray-6 bg-transparent px-2.5 py-1.5 text-gray-11 text-sm outline-none focus:text-gray-12 focus:placeholder:text-gray-10" - style={inputStyle} - /> -
    + return ( +
    + + {description && ( +
    {description}
    + )} + onChange(evt.target.value)} + disabled={disabled} + placeholder={placeholder} + spellCheck={false} + className="mt-1 block w-full rounded border border-gray-6 bg-transparent px-2.5 py-1.5 text-gray-11 text-sm outline-none focus:text-gray-12 focus:placeholder:text-gray-10" + style={inputStyle} + /> +
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/account/ProfileSection.tsx b/frontend/src/app/(navfooter)/settings/account/ProfileSection.tsx index fcb414078..22f39fbd5 100644 --- a/frontend/src/app/(navfooter)/settings/account/ProfileSection.tsx +++ b/frontend/src/app/(navfooter)/settings/account/ProfileSection.tsx @@ -1,32 +1,37 @@ -"use client" +"use client"; -import { LinkExternalIcon } from "@primer/octicons-react" +import { LinkExternalIcon } from "@primer/octicons-react"; -import Button from "@/components/Button" -import GhostButton from "@/components/GhostButton" -import { useThisUser, isAnonUser } from "@/lib/api" -import { userHtmlUrl } from "@/lib/api/urls" +import Button from "@/components/Button"; +import GhostButton from "@/components/GhostButton"; +import { useThisUser, isAnonUser } from "@/lib/api"; +import { userHtmlUrl } from "@/lib/api/urls"; -import Section from "../Section" +import Section from "../Section"; export default function ProfileSection() { - const user = useThisUser() + const user = useThisUser(); // No profile section for anonymous users if (!user || isAnonUser(user)) { - return null + return null; } - return
    -

    - Your name and profile picture are controlled by your GitHub account. -

    -
    - - View decomp.me profile -
    -
    + return ( +
    +

    + Your name and profile picture are controlled by your GitHub + account. +

    +
    + + + View decomp.me profile + +
    +
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/account/SignOutButton.tsx b/frontend/src/app/(navfooter)/settings/account/SignOutButton.tsx index 8d4148a2a..426b63ced 100644 --- a/frontend/src/app/(navfooter)/settings/account/SignOutButton.tsx +++ b/frontend/src/app/(navfooter)/settings/account/SignOutButton.tsx @@ -1,21 +1,23 @@ -"use client" +"use client"; -import { mutate } from "swr" +import { mutate } from "swr"; -import AsyncButton from "@/components/AsyncButton" -import { useThisUser, isAnonUser } from "@/lib/api" -import { post } from "@/lib/api/request" +import AsyncButton from "@/components/AsyncButton"; +import { useThisUser, isAnonUser } from "@/lib/api"; +import { post } from "@/lib/api/request"; export default function SignOutButton() { - const user = useThisUser() - const isAnon = user && isAnonUser(user) + const user = useThisUser(); + const isAnon = user && isAnonUser(user); - return { - const user = await post("/user", {}) - await mutate("/user", user) - }} - > - {isAnon ? "Reset anonymous appearance" : "Sign out"} - + return ( + { + const user = await post("/user", {}); + await mutate("/user", user); + }} + > + {isAnon ? "Reset anonymous appearance" : "Sign out"} + + ); } diff --git a/frontend/src/app/(navfooter)/settings/account/UserState.tsx b/frontend/src/app/(navfooter)/settings/account/UserState.tsx index f89adf727..75488f577 100644 --- a/frontend/src/app/(navfooter)/settings/account/UserState.tsx +++ b/frontend/src/app/(navfooter)/settings/account/UserState.tsx @@ -1,31 +1,38 @@ -"use client" +"use client"; -import GitHubLoginButton from "@/components/GitHubLoginButton" -import LoadingSpinner from "@/components/loading.svg" -import UserAvatar from "@/components/user/UserAvatar" -import UserMention from "@/components/user/UserMention" -import { isAnonUser, useThisUser } from "@/lib/api" +import GitHubLoginButton from "@/components/GitHubLoginButton"; +import LoadingSpinner from "@/components/loading.svg"; +import UserAvatar from "@/components/user/UserAvatar"; +import UserMention from "@/components/user/UserMention"; +import { isAnonUser, useThisUser } from "@/lib/api"; -import SignOutButton from "./SignOutButton" +import SignOutButton from "./SignOutButton"; export default function UserState() { - const user = useThisUser() + const user = useThisUser(); - return
    -
    - -
    - {user ?
    -

    - {isAnonUser(user) ? "You appear as" : "Signed in as"} -

    -
    - {isAnonUser(user) && } - + return ( +
    +
    +
    -
    :
    - - Loading... -
    } -
    + {user ? ( +
    +

    + {isAnonUser(user) ? "You appear as" : "Signed in as"}{" "} + +

    +
    + {isAnonUser(user) && } + +
    +
    + ) : ( +
    + + Loading... +
    + )} +
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/account/page.tsx b/frontend/src/app/(navfooter)/settings/account/page.tsx index ab2d15f92..6e093f0b4 100644 --- a/frontend/src/app/(navfooter)/settings/account/page.tsx +++ b/frontend/src/app/(navfooter)/settings/account/page.tsx @@ -1,17 +1,19 @@ -import Section from "../Section" +import Section from "../Section"; -import ProfileSection from "./ProfileSection" -import UserState from "./UserState" +import ProfileSection from "./ProfileSection"; +import UserState from "./UserState"; export const metadata = { title: "Account settings", -} +}; export default function Page() { - return <> -
    - -
    - - + return ( + <> +
    + +
    + + + ); } diff --git a/frontend/src/app/(navfooter)/settings/appearance/AppearanceSettings.tsx b/frontend/src/app/(navfooter)/settings/appearance/AppearanceSettings.tsx index d71765105..24b6f1b81 100644 --- a/frontend/src/app/(navfooter)/settings/appearance/AppearanceSettings.tsx +++ b/frontend/src/app/(navfooter)/settings/appearance/AppearanceSettings.tsx @@ -1,61 +1,89 @@ -"use client" +"use client"; -import dynamic from "next/dynamic" +import dynamic from "next/dynamic"; -import ColorSchemePicker from "@/components/ColorSchemePicker" -import LoadingSpinner from "@/components/loading.svg" -import ThemePicker from "@/components/ThemePicker" -import * as settings from "@/lib/settings" +import ColorSchemePicker from "@/components/ColorSchemePicker"; +import LoadingSpinner from "@/components/loading.svg"; +import ThemePicker from "@/components/ThemePicker"; +import * as settings from "@/lib/settings"; -import Section from "../Section" -import SliderField from "../SliderField" -import TextField from "../TextField" +import Section from "../Section"; +import SliderField from "../SliderField"; +import TextField from "../TextField"; const DynamicExampleCodeMirror = dynamic(() => import("./ExampleCodeMirror"), { - loading: () =>
    - -
    , -}) + loading: () => ( +
    + +
    + ), +}); export default function AppearanceSettings() { - const [theme, setTheme] = settings.useTheme() - const [fontSize, setFontSize] = settings.useCodeFontSize() - const [monospaceFont, setMonospaceFont] = settings.useMonospaceFont() - const [codeLineHeight, setCodeLineHeight] = settings.useCodeLineHeight() - const [codeColorScheme, setCodeColorScheme] = settings.useCodeColorScheme() - - return <> -
    - -
    -
    -
    -
    - + const [theme, setTheme] = settings.useTheme(); + const [fontSize, setFontSize] = settings.useCodeFontSize(); + const [monospaceFont, setMonospaceFont] = settings.useMonospaceFont(); + const [codeLineHeight, setCodeLineHeight] = settings.useCodeLineHeight(); + const [codeColorScheme, setCodeColorScheme] = settings.useCodeColorScheme(); + + return ( + <> +
    + +
    +
    +
    +
    + +
    +
    + +
    -
    - + +
    +
    -
    - -
    - -
    - -
    -
    Color scheme
    -
    - + +
    +
    Color scheme
    +
    + +
    +
    - -
    -
    - +
    + + ); } diff --git a/frontend/src/app/(navfooter)/settings/appearance/ExampleCodeMirror.tsx b/frontend/src/app/(navfooter)/settings/appearance/ExampleCodeMirror.tsx index 327c4db3b..d0c7818d9 100644 --- a/frontend/src/app/(navfooter)/settings/appearance/ExampleCodeMirror.tsx +++ b/frontend/src/app/(navfooter)/settings/appearance/ExampleCodeMirror.tsx @@ -1,10 +1,10 @@ -"use client" +"use client"; -import CodeMirror from "@/components/Editor/CodeMirror" -import basicSetup from "@/lib/codemirror/basic-setup" -import { cpp } from "@/lib/codemirror/cpp" +import CodeMirror from "@/components/Editor/CodeMirror"; +import basicSetup from "@/lib/codemirror/basic-setup"; +import { cpp } from "@/lib/codemirror/cpp"; -import styles from "./ExampleCodeMirror.module.scss" +import styles from "./ExampleCodeMirror.module.scss"; const EXAMPLE_C_CODE = `#include "common.h" @@ -110,14 +110,16 @@ void step_game_loop(void) { rand_int(1); } -` +`; export default function ExampleCodeMirror() { - return
    - -
    + return ( +
    + +
    + ); } diff --git a/frontend/src/app/(navfooter)/settings/appearance/page.tsx b/frontend/src/app/(navfooter)/settings/appearance/page.tsx index c01bf16d8..15602e7bd 100644 --- a/frontend/src/app/(navfooter)/settings/appearance/page.tsx +++ b/frontend/src/app/(navfooter)/settings/appearance/page.tsx @@ -1,11 +1,13 @@ -import AppearanceSettings from "./AppearanceSettings" +import AppearanceSettings from "./AppearanceSettings"; export const metadata = { title: "Appearance settings", -} +}; export default function Page() { - return <> - - + return ( + <> + + + ); } diff --git a/frontend/src/app/(navfooter)/settings/editor/EditorSettings.tsx b/frontend/src/app/(navfooter)/settings/editor/EditorSettings.tsx index 4148b167b..266786f37 100644 --- a/frontend/src/app/(navfooter)/settings/editor/EditorSettings.tsx +++ b/frontend/src/app/(navfooter)/settings/editor/EditorSettings.tsx @@ -1,122 +1,156 @@ -"use client" -import { useEffect, useRef, useState } from "react" +"use client"; +import { useEffect, useRef, useState } from "react"; -import LoadingSpinner from "@/components/loading.svg" +import LoadingSpinner from "@/components/loading.svg"; import { - ThreeWayDiffBase, useAutoRecompileSetting, useAutoRecompileDelaySetting, useMatchProgressBarEnabled, - useLanguageServerEnabled, useVimModeEnabled, useThreeWayDiffBase, useObjdiffClientEnabled, -} from "@/lib/settings" + ThreeWayDiffBase, + useAutoRecompileSetting, + useAutoRecompileDelaySetting, + useMatchProgressBarEnabled, + useLanguageServerEnabled, + useVimModeEnabled, + useThreeWayDiffBase, + useObjdiffClientEnabled, +} from "@/lib/settings"; -import Checkbox from "../Checkbox" -import RadioList from "../RadioList" -import Section from "../Section" -import SliderField from "../SliderField" +import Checkbox from "../Checkbox"; +import RadioList from "../RadioList"; +import Section from "../Section"; +import SliderField from "../SliderField"; export default function EditorSettings() { - const [autoRecompile, setAutoRecompile] = useAutoRecompileSetting() - const [autoRecompileDelay, setAutoRecompileDelay] = useAutoRecompileDelaySetting() - const [matchProgressBarEnabled, setMatchProgressBarEnabled] = useMatchProgressBarEnabled() - const [languageServerEnabled, setLanguageServerEnabled] = useLanguageServerEnabled() - const [vimModeEnabled, setVimModeEnabled] = useVimModeEnabled() - const [threeWayDiffBase, setThreeWayDiffBase] = useThreeWayDiffBase() - const [objdiffClientEnabled, setObjdiffClientEnabled] = useObjdiffClientEnabled() + const [autoRecompile, setAutoRecompile] = useAutoRecompileSetting(); + const [autoRecompileDelay, setAutoRecompileDelay] = + useAutoRecompileDelaySetting(); + const [matchProgressBarEnabled, setMatchProgressBarEnabled] = + useMatchProgressBarEnabled(); + const [languageServerEnabled, setLanguageServerEnabled] = + useLanguageServerEnabled(); + const [vimModeEnabled, setVimModeEnabled] = useVimModeEnabled(); + const [threeWayDiffBase, setThreeWayDiffBase] = useThreeWayDiffBase(); + const [objdiffClientEnabled, setObjdiffClientEnabled] = + useObjdiffClientEnabled(); - const [downloadingLanguageServer, setDownloadingLanguageServer] = useState(false) + const [downloadingLanguageServer, setDownloadingLanguageServer] = + useState(false); - const isInitialMount = useRef(true) + const isInitialMount = useRef(true); useEffect(() => { // Prevent the language server binary from being downloaded if the user has it enabled, then enters settings if (isInitialMount.current) { - isInitialMount.current = false - return + isInitialMount.current = false; + return; } if (languageServerEnabled) { - setDownloadingLanguageServer(true) + setDownloadingLanguageServer(true); - import("@clangd-wasm/clangd-wasm").then(({ ClangdStdioTransport }) => { - // We don't need to do anything with the result of this fetch - all this - // is is a way to make sure the wasm file ends up in the browser's cache. - fetch(ClangdStdioTransport.getDefaultWasmURL(false)) - .then(res => res.blob()) - .then(() => setDownloadingLanguageServer(false)) - }) + import("@clangd-wasm/clangd-wasm").then( + ({ ClangdStdioTransport }) => { + // We don't need to do anything with the result of this fetch - all this + // is is a way to make sure the wasm file ends up in the browser's cache. + fetch(ClangdStdioTransport.getDefaultWasmURL(false)) + .then((res) => res.blob()) + .then(() => setDownloadingLanguageServer(false)); + }, + ); } - }, [languageServerEnabled]) + }, [languageServerEnabled]); const threeWayDiffOptions = { - [ThreeWayDiffBase.SAVED]: { label:
    Latest save ( diff.py -b )
    }, - [ThreeWayDiffBase.PREV]: { label:
    Previous result ( diff.py -3 )
    }, - } - - return <> -
    - -
    - + [ThreeWayDiffBase.SAVED]: { + label: ( +
    + Latest save ({" "} + diff.py -b ) +
    + ), + }, + [ThreeWayDiffBase.PREV]: { + label: ( +
    + Previous result ({" "} + diff.py -3 )
    - -
    -
    -
    - When enabling three-way diffing for a scratch, let the third column show a diff against: -
    - { - setThreeWayDiffBase(value as ThreeWayDiffBase) - }} - options={threeWayDiffOptions} - /> -
    -
    - -
    -
    - + ), + }, + }; - {downloadingLanguageServer &&
    Downloading...
    } -
    -
    -
    - - -
    -
    - - -
    - + return ( + <> +
    + +
    + +
    +
    +
    +
    +
    + When enabling three-way diffing for a scratch, let the third + column show a diff against: +
    + { + setThreeWayDiffBase(value as ThreeWayDiffBase); + }} + options={threeWayDiffOptions} + /> +
    +
    + +
    +
    + + {downloadingLanguageServer && ( +
    + Downloading... +
    + )} +
    +
    +
    + +
    +
    + +
    + + ); } diff --git a/frontend/src/app/(navfooter)/settings/editor/page.tsx b/frontend/src/app/(navfooter)/settings/editor/page.tsx index 2356906f3..0dfd709ce 100644 --- a/frontend/src/app/(navfooter)/settings/editor/page.tsx +++ b/frontend/src/app/(navfooter)/settings/editor/page.tsx @@ -1,11 +1,13 @@ -import EditorSettings from "./EditorSettings" +import EditorSettings from "./EditorSettings"; export const metadata = { title: "Editor settings", -} +}; export default function Page() { - return <> - - + return ( + <> + + + ); } diff --git a/frontend/src/app/(navfooter)/settings/layout.tsx b/frontend/src/app/(navfooter)/settings/layout.tsx index 188297b4f..2c9c33226 100644 --- a/frontend/src/app/(navfooter)/settings/layout.tsx +++ b/frontend/src/app/(navfooter)/settings/layout.tsx @@ -1,24 +1,38 @@ -import { FileIcon, PaintbrushIcon, GearIcon } from "@primer/octicons-react" +import { FileIcon, PaintbrushIcon, GearIcon } from "@primer/octicons-react"; -import NavItem from "./NavItem" +import NavItem from "./NavItem"; export default function Layout({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { - return
    - -
    - {children} -
    -
    + return ( +
    + +
    + {children} +
    +
    + ); } diff --git a/frontend/src/app/(navfooter)/u/[username]/page.tsx b/frontend/src/app/(navfooter)/u/[username]/page.tsx index 94fe55375..eea483aa7 100644 --- a/frontend/src/app/(navfooter)/u/[username]/page.tsx +++ b/frontend/src/app/(navfooter)/u/[username]/page.tsx @@ -1,22 +1,24 @@ -import type { Metadata } from "next" +import type { Metadata } from "next"; -import { notFound } from "next/navigation" +import { notFound } from "next/navigation"; -import Profile from "@/components/user/Profile" -import { get } from "@/lib/api/request" -import type { User } from "@/lib/api/types" +import Profile from "@/components/user/Profile"; +import { get } from "@/lib/api/request"; +import type { User } from "@/lib/api/types"; -export async function generateMetadata({ params }: { params: { username: string } }): Promise { - let user: User +export async function generateMetadata({ + params, +}: { params: { username: string } }): Promise { + let user: User; try { - user = await get(`/users/${params.username}`) + user = await get(`/users/${params.username}`); } catch (error) { - console.error(error) + console.error(error); } if (!user) { - return notFound() + return notFound(); } return { @@ -24,20 +26,22 @@ export async function generateMetadata({ params }: { params: { username: string openGraph: { title: user.username, }, - } + }; } -export default async function Page({ params }: { params: { username: string } }) { - let user: User +export default async function Page({ + params, +}: { params: { username: string } }) { + let user: User; try { - user = await get(`/users/${params.username}`) + user = await get(`/users/${params.username}`); } catch (error) { - console.error(error) + console.error(error); } if (!user) { - return notFound() + return notFound(); } - return + return ; } diff --git a/frontend/src/app/ThemeProvider.tsx b/frontend/src/app/ThemeProvider.tsx index 3271ad2db..0e37d8f06 100644 --- a/frontend/src/app/ThemeProvider.tsx +++ b/frontend/src/app/ThemeProvider.tsx @@ -1,48 +1,54 @@ -"use client" +"use client"; -import { useEffect } from "react" +import { useEffect } from "react"; -import { applyColorScheme } from "@/lib/codemirror/color-scheme" -import * as settings from "@/lib/settings" +import { applyColorScheme } from "@/lib/codemirror/color-scheme"; +import * as settings from "@/lib/settings"; export default function ThemeProvider() { - const [codeColorScheme, setCodeColorScheme] = settings.useCodeColorScheme() + const [codeColorScheme, setCodeColorScheme] = settings.useCodeColorScheme(); useEffect(() => { - applyColorScheme(codeColorScheme) - }, [codeColorScheme]) + applyColorScheme(codeColorScheme); + }, [codeColorScheme]); - const isSiteThemeDark = settings.useIsSiteThemeDark() + const isSiteThemeDark = settings.useIsSiteThemeDark(); useEffect(() => { // Apply theme if (isSiteThemeDark) { - document.documentElement.classList.add("dark") + document.documentElement.classList.add("dark"); } else { - document.documentElement.classList.remove("dark") + document.documentElement.classList.remove("dark"); } // If using the default code color scheme (Frog), pick the variant that matches the site theme - setCodeColorScheme(current => { + setCodeColorScheme((current) => { if (current === "Frog Dark" || current === "Frog Light") { - return isSiteThemeDark ? "Frog Dark" : "Frog Light" + return isSiteThemeDark ? "Frog Dark" : "Frog Light"; } else { - return current + return current; } - }) - }, [isSiteThemeDark, setCodeColorScheme]) + }); + }, [isSiteThemeDark, setCodeColorScheme]); - const [monospaceFont] = settings.useMonospaceFont() + const [monospaceFont] = settings.useMonospaceFont(); useEffect(() => { - document.body.style.removeProperty("--monospace") + document.body.style.removeProperty("--monospace"); if (monospaceFont) { - document.body.style.setProperty("--monospace", `${monospaceFont}, monospace`) + document.body.style.setProperty( + "--monospace", + `${monospaceFont}, monospace`, + ); } - }, [monospaceFont]) + }, [monospaceFont]); - const [codeLineHeight] = settings.useCodeLineHeight() + const [codeLineHeight] = settings.useCodeLineHeight(); useEffect(() => { - document.body.style.removeProperty("--code-line-height") - document.body.style.setProperty("--code-line-height", `${codeLineHeight}`) - }, [codeLineHeight]) - - return <> + document.body.style.removeProperty("--code-line-height"); + document.body.style.setProperty( + "--code-line-height", + `${codeLineHeight}`, + ); + }, [codeLineHeight]); + + return <>; } diff --git a/frontend/src/app/error.tsx b/frontend/src/app/error.tsx index 84414fc3b..27ff571f3 100644 --- a/frontend/src/app/error.tsx +++ b/frontend/src/app/error.tsx @@ -1,74 +1,104 @@ -"use client" +"use client"; -import { useEffect } from "react" +import { useEffect } from "react"; -import { SyncIcon } from "@primer/octicons-react" +import { SyncIcon } from "@primer/octicons-react"; -import Button from "@/components/Button" -import ErrorBoundary from "@/components/ErrorBoundary" -import SetPageTitle from "@/components/SetPageTitle" -import { RequestFailedError } from "@/lib/api" +import Button from "@/components/Button"; +import ErrorBoundary from "@/components/ErrorBoundary"; +import SetPageTitle from "@/components/SetPageTitle"; +import { RequestFailedError } from "@/lib/api"; -type ErrorPageProps = {error: Error, reset: () => void }; +type ErrorPageProps = { error: Error; reset: () => void }; function NetworkErrorPage({ error }: ErrorPageProps) { - return <> - -
    -
    -

    We're having some trouble reaching the backend

    + return ( + <> + +
    +
    +

    + We're having some trouble reaching the backend +

    -
    - {error.toString()} -
    +
    + + {error.toString()} + +
    -

    - If your internet connection is okay, we're probably down for maintenance, and will be back shortly. If this issue persists - let us know. -

    +

    + If your internet connection is okay, we're probably down for + maintenance, and will be back shortly. If this issue + persists -{" "} + + let us know + + . +

    - - - -
    -
    - + + + +
    +
    + + ); } function UnexpectedErrorPage({ error, reset }: ErrorPageProps) { - return <> - -
    -
    -

    Something went wrong

    -

    - An unexpected error occurred rendering this page. -

    -
    - {error.toString()} -
    -

    - If this keeps happening, let us know. -

    - - - -
    -
    - + return ( + <> + +
    +
    +

    Something went wrong

    +

    + An unexpected error occurred rendering this page. +

    +
    + + {error.toString()} + +
    +

    + If this keeps happening,{" "} + + let us know + + . +

    + + + +
    +
    + + ); } export default function ErrorPage({ error, reset }: ErrorPageProps) { useEffect(() => { - console.error(error) - }, [error]) + console.error(error); + }, [error]); - return error instanceof RequestFailedError ? : + return error instanceof RequestFailedError ? ( + + ) : ( + + ); } export const metadata = { title: "Error", -} +}; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index f849fd123..d71fdb1a8 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,9 +1,9 @@ -import PlausibleProvider from "next-plausible" +import PlausibleProvider from "next-plausible"; -import ThemeProvider from "./ThemeProvider" +import ThemeProvider from "./ThemeProvider"; -import "allotment/dist/style.css" -import "./globals.scss" +import "allotment/dist/style.css"; +import "./globals.scss"; export const metadata = { title: { @@ -22,37 +22,43 @@ export const metadata = { ], }, // set this to avoid "metadata.metadataBase is not set..." warning - metadataBase: new URL(process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : `http://localhost:${process.env.PORT || 3000}`), -} + metadataBase: new URL( + process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : `http://localhost:${process.env.PORT || 3000}`, + ), +}; export default function RootLayout({ children, }: { - children: React.ReactNode + children: React.ReactNode; }) { - return - - - - - - - - - - - - - - - - - {children} - - + return ( + + + + + + + + + + + + + + + + + + {children} + + + ); } diff --git a/frontend/src/app/not-found.tsx b/frontend/src/app/not-found.tsx index 3ae63a835..af8c49e75 100644 --- a/frontend/src/app/not-found.tsx +++ b/frontend/src/app/not-found.tsx @@ -1,28 +1,31 @@ -import { ChevronRightIcon } from "@primer/octicons-react" +import { ChevronRightIcon } from "@primer/octicons-react"; -import GhostButton from "@/components/GhostButton" -import Frog from "@/components/Nav/frog.svg" +import GhostButton from "@/components/GhostButton"; +import Frog from "@/components/Nav/frog.svg"; export default function NotFound() { - return
    -
    -
    - -

    - 404 - frog not found -

    -
    + return ( +
    +
    +
    + +

    + 404 + frog not found +

    +
    -

    - The page you are looking for is not here. Consider checking the URL. -

    +

    + The page you are looking for is not here. Consider checking + the URL. +

    -
    - - Back to dashboard - +
    + + Back to dashboard + +
    -
    -
    +
    + ); } diff --git a/frontend/src/app/opengraph-image.tsx b/frontend/src/app/opengraph-image.tsx index f30bacdc1..58c2c8cd6 100644 --- a/frontend/src/app/opengraph-image.tsx +++ b/frontend/src/app/opengraph-image.tsx @@ -1,61 +1,86 @@ -import { ImageResponse } from "next/og" +import { ImageResponse } from "next/og"; -import { platformIcon, PLATFORMS } from "@/components/PlatformSelect/PlatformIcon" +import { + platformIcon, + PLATFORMS, +} from "@/components/PlatformSelect/PlatformIcon"; -const IMAGE_WIDTH_PX = 1200 +const IMAGE_WIDTH_PX = 1200; -const IMAGE_HEIGHT_PX = 400 +const IMAGE_HEIGHT_PX = 400; -export const runtime = "edge" +export const runtime = "edge"; export default async function HomeOG() { - const OpenSansExtraBold = fetch(new URL("/public/fonts/OpenSans-ExtraBold.ttf", import.meta.url)).then(res => - res.arrayBuffer() - ) + const OpenSansExtraBold = fetch( + new URL("/public/fonts/OpenSans-ExtraBold.ttf", import.meta.url), + ).then((res) => res.arrayBuffer()); - const OpenSansSemiBold = fetch(new URL("/public/fonts/OpenSans-SemiBold.ttf", import.meta.url)).then(res => - res.arrayBuffer() - ) + const OpenSansSemiBold = fetch( + new URL("/public/fonts/OpenSans-SemiBold.ttf", import.meta.url), + ).then((res) => res.arrayBuffer()); - const OpenSansBold = fetch(new URL("/public/fonts/OpenSans-Bold.ttf", import.meta.url)).then(res => - res.arrayBuffer() - ) + const OpenSansBold = fetch( + new URL("/public/fonts/OpenSans-Bold.ttf", import.meta.url), + ).then((res) => res.arrayBuffer()); - const statsRes = await fetch("http://decomp.me/api/stats") - const stats = await statsRes.json() - const iconSize = 160 - const iconCount = 5 - const textScale = 4.15 + const statsRes = await fetch("http://decomp.me/api/stats"); + const stats = await statsRes.json(); + const iconSize = 160; + const iconCount = 5; + const textScale = 4.15; const textSize = { title: textScale, description: 0.6 * textScale, stats: 0.45 * textScale, - } + }; return new ImageResponse( - ( +
    +
    + {PLATFORMS.map((platform) => ({ + platform, + sort: Math.random(), + })) + .sort((a, b) => a.sort - b.sort) + .slice(0, iconCount) + .map(({ platform }) => { + const Icon = platformIcon(platform); + return ( + + ); + })} +
    +
    +
    + decomp.me +
    + + Collaboratively decompile code in your browser + +
    -
    - {PLATFORMS.map(platform => ({ platform, sort: Math.random() })) - .sort((a, b) => a.sort - b.sort) - .slice(0, iconCount) - .map(({ platform }) => { - const Icon = platformIcon(platform) - return () - })} -
    - , { width: IMAGE_WIDTH_PX, height: IMAGE_HEIGHT_PX, @@ -63,7 +88,6 @@ export default async function HomeOG() { { name: "OpenSans-ExtraBold", data: await OpenSansExtraBold, - }, { name: "OpenSans-SemiBold", @@ -75,5 +99,5 @@ export default async function HomeOG() { }, ], }, - ) + ); } diff --git a/frontend/src/app/scratch/[slug]/ScratchEditor.tsx b/frontend/src/app/scratch/[slug]/ScratchEditor.tsx index ac30249f1..4a2d81bba 100644 --- a/frontend/src/app/scratch/[slug]/ScratchEditor.tsx +++ b/frontend/src/app/scratch/[slug]/ScratchEditor.tsx @@ -1,32 +1,37 @@ -"use client" +"use client"; -import { useEffect, useState } from "react" +import { useEffect, useState } from "react"; -import useSWR, { type Middleware, SWRConfig } from "swr" +import useSWR, { type Middleware, SWRConfig } from "swr"; -import Scratch from "@/components/Scratch" -import useWarnBeforeScratchUnload from "@/components/Scratch/hooks/useWarnBeforeScratchUnload" -import SetPageTitle from "@/components/SetPageTitle" -import * as api from "@/lib/api" -import { scratchUrl } from "@/lib/api/urls" +import Scratch from "@/components/Scratch"; +import useWarnBeforeScratchUnload from "@/components/Scratch/hooks/useWarnBeforeScratchUnload"; +import SetPageTitle from "@/components/SetPageTitle"; +import * as api from "@/lib/api"; +import { scratchUrl } from "@/lib/api/urls"; function ScratchPageTitle({ scratch }: { scratch: api.Scratch }) { - const isSaved = api.useIsScratchSaved(scratch) + const isSaved = api.useIsScratchSaved(scratch); - let title = isSaved ? "" : "(unsaved) " - title += scratch.name || scratch.slug + let title = isSaved ? "" : "(unsaved) "; + title += scratch.name || scratch.slug; - return + return ; } -function ScratchEditorInner({ initialScratch, parentScratch, initialCompilation, offline }: Props) { - const [scratch, setScratch] = useState(initialScratch) +function ScratchEditorInner({ + initialScratch, + parentScratch, + initialCompilation, + offline, +}: Props) { + const [scratch, setScratch] = useState(initialScratch); - useWarnBeforeScratchUnload(scratch) + useWarnBeforeScratchUnload(scratch); // If the static props scratch changes (i.e. router push / page redirect), reset `scratch`. if (scratchUrl(scratch) !== scratchUrl(initialScratch)) - setScratch(initialScratch) + setScratch(initialScratch); // If the server scratch owner changes (i.e. scratch was claimed), update local scratch owner. // You can trigger this by: @@ -34,11 +39,18 @@ function ScratchEditorInner({ initialScratch, parentScratch, initialCompilation, // 2. Creating a new scratch // 3. Logging in // 4. Notice the scratch owner (in the About panel) has changed to your newly-logged-in user - const ownerMayChange = !scratch.owner || scratch.owner.is_anonymous - const cached = useSWR(ownerMayChange && scratchUrl(scratch), api.get)?.data - if (ownerMayChange && cached?.owner && !api.isUserEq(scratch.owner, cached?.owner)) { - console.info("Scratch owner updated", cached.owner) - setScratch(scratch => ({ ...scratch, owner: cached.owner })) + const ownerMayChange = !scratch.owner || scratch.owner.is_anonymous; + const cached = useSWR( + ownerMayChange && scratchUrl(scratch), + api.get, + )?.data; + if ( + ownerMayChange && + cached?.owner && + !api.isUserEq(scratch.owner, cached?.owner) + ) { + console.info("Scratch owner updated", cached.owner); + setScratch((scratch) => ({ ...scratch, owner: cached.owner })); } // On initial page load, request the latest scratch from the server, and @@ -48,64 +60,68 @@ function ScratchEditorInner({ initialScratch, parentScratch, initialCompilation, // https://github.com/decompme/decomp.me/issues/711 useEffect(() => { api.get(scratchUrl(scratch)).then((updatedScratch: api.Scratch) => { - const updateTime = new Date(updatedScratch.last_updated) - const scratchTime = new Date(scratch.last_updated) + const updateTime = new Date(updatedScratch.last_updated); + const scratchTime = new Date(scratch.last_updated); if (scratchTime < updateTime) { - console.info("Client got updated scratch", updatedScratch) - setScratch(updatedScratch) + console.info("Client got updated scratch", updatedScratch); + setScratch(updatedScratch); } - }) - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - return <> - -
    - { - setScratch(scratch => { - return { ...scratch, ...partial } - }) - }} - offline={offline} - /> -
    - + }); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + +
    + { + setScratch((scratch) => { + return { ...scratch, ...partial }; + }); + }} + offline={offline} + /> +
    + + ); } export interface Props { - initialScratch: api.Scratch - parentScratch?: api.Scratch - initialCompilation?: api.Compilation - offline?: boolean + initialScratch: api.Scratch; + parentScratch?: api.Scratch; + initialCompilation?: api.Compilation; + offline?: boolean; } export default function ScratchEditor(props: Props) { - const [offline, setOffline] = useState(false) + const [offline, setOffline] = useState(false); - const offlineMiddleware: Middleware = _useSWRNext => { + const offlineMiddleware: Middleware = (_useSWRNext) => { return (key, fetcher, config) => { - let swr = _useSWRNext(key, fetcher, config) + let swr = _useSWRNext(key, fetcher, config); if (swr.error instanceof api.RequestFailedError) { - setOffline(true) - swr = Object.assign({}, swr, { error: null }) + setOffline(true); + swr = Object.assign({}, swr, { error: null }); } - return swr - } - } + return swr; + }; + }; const onSuccess = () => { - setOffline(false) - } - - return <> - - - - + setOffline(false); + }; + + return ( + <> + + + + + ); } diff --git a/frontend/src/app/scratch/[slug]/claim/page.tsx b/frontend/src/app/scratch/[slug]/claim/page.tsx index 10eb7b3fc..105eead3e 100644 --- a/frontend/src/app/scratch/[slug]/claim/page.tsx +++ b/frontend/src/app/scratch/[slug]/claim/page.tsx @@ -1,46 +1,49 @@ -"use client" +"use client"; -import { useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState } from "react"; -import { useRouter } from "next/navigation" +import { useRouter } from "next/navigation"; -import LoadingSkeleton from "@/app/scratch/[slug]/loading" -import { post } from "@/lib/api/request" +import LoadingSkeleton from "@/app/scratch/[slug]/loading"; +import { post } from "@/lib/api/request"; -export default function Page({ params, searchParams }: { - params: { slug: string } - searchParams: { token: string } +export default function Page({ + params, + searchParams, +}: { + params: { slug: string }; + searchParams: { token: string }; }) { - const router = useRouter() + const router = useRouter(); // The POST request must happen on the client so // that the Django session cookie is present. - const effectRan = useRef(false) - const [error, setError] = useState(null) + const effectRan = useRef(false); + const [error, setError] = useState(null); useEffect(() => { if (!effectRan.current) { post(`/scratch/${params.slug}/claim`, { token: searchParams.token }) - .then(data => { + .then((data) => { if (data.success) { - router.replace(`/scratch/${params.slug}`) + router.replace(`/scratch/${params.slug}`); } else { - throw new Error("Unable to claim scratch") + throw new Error("Unable to claim scratch"); } }) - .catch(err => { - console.error("Failed to claim scratch", err) - setError(err) - }) + .catch((err) => { + console.error("Failed to claim scratch", err); + setError(err); + }); } return () => { - effectRan.current = true - } - }, [params.slug, router, searchParams.token]) + effectRan.current = true; + }; + }, [params.slug, router, searchParams.token]); if (error) { // Rely on error boundary to catch and display error - throw error + throw error; } - return + return ; } diff --git a/frontend/src/app/scratch/[slug]/getScratchDetails.ts b/frontend/src/app/scratch/[slug]/getScratchDetails.ts index 68c30e539..15f598e30 100644 --- a/frontend/src/app/scratch/[slug]/getScratchDetails.ts +++ b/frontend/src/app/scratch/[slug]/getScratchDetails.ts @@ -1,26 +1,30 @@ -import { get, bubbleNotFound, ResponseError } from "@/lib/api/request" -import type { Scratch, Compilation } from "@/lib/api/types" -import { scratchParentUrl, scratchUrl } from "@/lib/api/urls" +import { get, bubbleNotFound, ResponseError } from "@/lib/api/request"; +import type { Scratch, Compilation } from "@/lib/api/types"; +import { scratchParentUrl, scratchUrl } from "@/lib/api/urls"; export default async function getScratchDetails(slug: string) { - const scratch: Scratch = await get(`/scratch/${slug}`).catch(bubbleNotFound) + const scratch: Scratch = await get(`/scratch/${slug}`).catch( + bubbleNotFound, + ); - let compilation: Compilation | null = null + let compilation: Compilation | null = null; try { - compilation = await get(`${scratchUrl(scratch)}/compile`) + compilation = await get(`${scratchUrl(scratch)}/compile`); } catch (error) { if (error instanceof ResponseError && error.status !== 400) { - compilation = null + compilation = null; } else { - throw error + throw error; } } - const parentScratch: Scratch | null = scratch.parent ? await get(scratchParentUrl(scratch)) : null + const parentScratch: Scratch | null = scratch.parent + ? await get(scratchParentUrl(scratch)) + : null; return { scratch, parentScratch, compilation, - } + }; } diff --git a/frontend/src/app/scratch/[slug]/loading.tsx b/frontend/src/app/scratch/[slug]/loading.tsx index e0689adeb..72e707ca1 100644 --- a/frontend/src/app/scratch/[slug]/loading.tsx +++ b/frontend/src/app/scratch/[slug]/loading.tsx @@ -1,6 +1,6 @@ -import { useMemo } from "react" +import { useMemo } from "react"; -import Nav from "@/components/Nav" +import Nav from "@/components/Nav"; const CODE = `#include "common.h" @@ -27,7 +27,7 @@ void step_game_loop(void) { return; } } -}` +}`; const DIFF = ` 0: stwu r1,-0x20(r1) 4: mflr r0 @@ -61,90 +61,100 @@ const DIFF = ` 0: stwu r1,-0x20(r1) 74: ~> lwz r0,0x24(r1) 78: mtlr r0 7c: addi r1,r1,0x20 -80: blr` +80: blr`; function TextSkeleton({ text }: { text: string }) { - const lines = useMemo(() => ( - text - .split("\n") - .map(line => { + const lines = useMemo( + () => + text.split("\n").map((line) => { // Convert line into a sequence of [word len, space len] pairs. // e.g. "xxxx xx xxx x" -> [[4, 2], [2, 1], [3, 1], [1, 0]] - const pairs = [] + const pairs = []; - let state: "word" | "space" = "word" - let wordLen = 0 - let spaceLen = 0 + let state: "word" | "space" = "word"; + let wordLen = 0; + let spaceLen = 0; for (const char of line) { if (char === " ") { if (state === "word") { - pairs.push([wordLen, spaceLen]) - wordLen = 0 - spaceLen = 0 + pairs.push([wordLen, spaceLen]); + wordLen = 0; + spaceLen = 0; } - state = "space" - spaceLen++ - } else { // non-space + state = "space"; + spaceLen++; + } else { + // non-space if (state === "space") { - pairs.push([wordLen, spaceLen]) - wordLen = 0 - spaceLen = 0 + pairs.push([wordLen, spaceLen]); + wordLen = 0; + spaceLen = 0; } - state = "word" - wordLen++ + state = "word"; + wordLen++; } } - pairs.push([wordLen, spaceLen]) - - return pairs.filter(([wordLen, spaceLen]) => wordLen > 0 || spaceLen > 0) - }) - ), [text]) - - return
    - {lines.map((pairs, i) => -
    - {pairs.map(([wordLen, spaceLen], j) => -
    - )} -
    - )} -
    + pairs.push([wordLen, spaceLen]); + + return pairs.filter( + ([wordLen, spaceLen]) => wordLen > 0 || spaceLen > 0, + ); + }), + [text], + ); + + return ( +
    + {lines.map((pairs, i) => ( +
    + {pairs.map(([wordLen, spaceLen], j) => ( +
    + ))} +
    + ))} +
    + ); } export default function LoadingSkeleton() { - return
    - -
    -
    - -
    -
    - + return ( +
    + +
    +
    + +
    +
    + +
    -
    - - - Loading editor... - -
    + + + Loading editor... + +
    + ); } diff --git a/frontend/src/app/scratch/[slug]/opengraph-image.tsx b/frontend/src/app/scratch/[slug]/opengraph-image.tsx index 9f2f9e055..407ef2f52 100644 --- a/frontend/src/app/scratch/[slug]/opengraph-image.tsx +++ b/frontend/src/app/scratch/[slug]/opengraph-image.tsx @@ -1,48 +1,68 @@ -import { ImageResponse } from "next/og" +import { ImageResponse } from "next/og"; -import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon" -import { percentToString, calculateScorePercent } from "@/components/ScoreBadge" -import { get } from "@/lib/api/request" -import type { Preset } from "@/lib/api/types" +import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon"; +import { + percentToString, + calculateScorePercent, +} from "@/components/ScoreBadge"; +import { get } from "@/lib/api/request"; +import type { Preset } from "@/lib/api/types"; -import CheckCircleFillIcon from "./assets/check-circle-fill.svg" -import PurpleFrog from "./assets/purplefrog.svg" -import RepoForkedIcon from "./assets/repo-forked.svg" -import TrophyIcon from "./assets/trophy.svg" -import XCircleFillIcon from "./assets/x-circle-fill.svg" -import getScratchDetails from "./getScratchDetails" +import CheckCircleFillIcon from "./assets/check-circle-fill.svg"; +import PurpleFrog from "./assets/purplefrog.svg"; +import RepoForkedIcon from "./assets/repo-forked.svg"; +import TrophyIcon from "./assets/trophy.svg"; +import XCircleFillIcon from "./assets/x-circle-fill.svg"; +import getScratchDetails from "./getScratchDetails"; -const truncateText = (text: string, length: number) => `${text.slice(0, length)}...${text.slice(-length, text.length)}` +const truncateText = (text: string, length: number) => + `${text.slice(0, length)}...${text.slice(-length, text.length)}`; -const IMAGE_WIDTH_PX = 1200 -const IMAGE_HEIGHT_PX = 400 +const IMAGE_WIDTH_PX = 1200; +const IMAGE_HEIGHT_PX = 400; -export const runtime = "edge" +export const runtime = "edge"; -export default async function ScratchOG({ params }: { params: { slug: string }}) { +export default async function ScratchOG({ + params, +}: { params: { slug: string } }) { + const { scratch, parentScratch, compilation } = await getScratchDetails( + params.slug, + ); - const { scratch, parentScratch, compilation } = await getScratchDetails(params.slug) - - const preset: Preset | null = scratch.preset !== null ? await get(`/preset/${scratch.preset}`) : null - const scratchName = scratch.name.length > 40 ? truncateText(scratch.name, 18) : scratch.name + const preset: Preset | null = + scratch.preset !== null ? await get(`/preset/${scratch.preset}`) : null; + const scratchName = + scratch.name.length > 40 + ? truncateText(scratch.name, 18) + : scratch.name; const scratchNameSize = - scratchName.length > 32 ? "4xl" : - scratchName.length > 24 ? "5xl" : "6xl" + scratchName.length > 32 + ? "4xl" + : scratchName.length > 24 + ? "5xl" + : "6xl"; - const score = compilation?.diff_output?.current_score ?? -1 - const maxScore = compilation?.diff_output?.max_score ?? -1 + const score = compilation?.diff_output?.current_score ?? -1; + const maxScore = compilation?.diff_output?.max_score ?? -1; - const percent = scratch.match_override ? 100 : calculateScorePercent(score, maxScore) - const doneWidth = Math.floor(percent * IMAGE_WIDTH_PX / 100) - const todoWidth = IMAGE_WIDTH_PX - doneWidth + const percent = scratch.match_override + ? 100 + : calculateScorePercent(score, maxScore); + const doneWidth = Math.floor((percent * IMAGE_WIDTH_PX) / 100); + const todoWidth = IMAGE_WIDTH_PX - doneWidth; return new ImageResponse(
    -
    {scratch.owner?.username ?? "No Owner"}
    -
    {scratchName}
    +
    + {scratch.owner?.username ?? "No Owner"} +
    +
    + {scratchName} +
    @@ -53,13 +73,14 @@ export default async function ScratchOG({ params }: { params: { slug: string }})
    -
    {preset?.name || "Custom Preset"}
    +
    + {preset?.name || "Custom Preset"} +
    - {score === -1 - ? + {score === -1 ? (
    @@ -68,42 +89,44 @@ export default async function ScratchOG({ params }: { params: { slug: string }})
    No Score
    - : score === 0 || scratch.match_override - ? -
    -
    - -
    -
    -
    Matched
    - {scratch.match_override && -
    (Override)
    - } -
    + ) : score === 0 || scratch.match_override ? ( +
    +
    +
    - : -
    -
    - -
    -
    -
    - {score} ({percentToString(percent)}) +
    +
    Matched
    + {scratch.match_override && ( +
    + (Override)
    + )} +
    +
    + ) : ( +
    +
    + +
    +
    +
    + {score} ({percentToString(percent)})
    - } - - {parentScratch && -
    -
    -
    -
    - {parentScratch.owner?.username ?? "Anonymous User"} + )} + + {parentScratch && ( +
    +
    + +
    +
    + {parentScratch.owner?.username ?? + "Anonymous User"} +
    -
    - } + )}
    @@ -120,5 +143,5 @@ export default async function ScratchOG({ params }: { params: { slug: string }}) width: IMAGE_WIDTH_PX, height: IMAGE_HEIGHT_PX, }, - ) + ); } diff --git a/frontend/src/app/scratch/[slug]/page.tsx b/frontend/src/app/scratch/[slug]/page.tsx index 3e214087e..46104e23b 100644 --- a/frontend/src/app/scratch/[slug]/page.tsx +++ b/frontend/src/app/scratch/[slug]/page.tsx @@ -1,11 +1,14 @@ -import type { Metadata, ResolvingMetadata } from "next" +import type { Metadata, ResolvingMetadata } from "next"; -import getScratchDetails from "./getScratchDetails" -import ScratchEditor from "./ScratchEditor" +import getScratchDetails from "./getScratchDetails"; +import ScratchEditor from "./ScratchEditor"; -export async function generateMetadata({ params }: { params: { slug: string }}, parent: ResolvingMetadata):Promise { - const { scratch } = await getScratchDetails(params.slug) - const parentData = await parent +export async function generateMetadata( + { params }: { params: { slug: string } }, + parent: ResolvingMetadata, +): Promise { + const { scratch } = await getScratchDetails(params.slug); + const parentData = await parent; return { title: scratch.name, @@ -19,17 +22,20 @@ export async function generateMetadata({ params }: { params: { slug: string }}, height: 400, }, ], - }, - } + }; } -export default async function Page({ params }: { params: { slug: string }}) { - const { scratch, parentScratch, compilation } = await getScratchDetails(params.slug) +export default async function Page({ params }: { params: { slug: string } }) { + const { scratch, parentScratch, compilation } = await getScratchDetails( + params.slug, + ); - return + return ( + + ); } diff --git a/frontend/src/components/AsyncButton.tsx b/frontend/src/components/AsyncButton.tsx index 0b39020a6..6ea167ecd 100644 --- a/frontend/src/components/AsyncButton.tsx +++ b/frontend/src/components/AsyncButton.tsx @@ -1,85 +1,98 @@ -"use client" +"use client"; -import { type ReactNode, useState, useCallback } from "react" +import { type ReactNode, useState, useCallback } from "react"; -import classNames from "classnames" -import { motion, AnimatePresence } from "framer-motion" -import { useLayer, Arrow } from "react-laag" +import classNames from "classnames"; +import { motion, AnimatePresence } from "framer-motion"; +import { useLayer, Arrow } from "react-laag"; -import styles from "./AsyncButton.module.scss" -import Button, { type Props as ButtonProps } from "./Button" -import LoadingSpinner from "./loading.svg" +import styles from "./AsyncButton.module.scss"; +import Button, { type Props as ButtonProps } from "./Button"; +import LoadingSpinner from "./loading.svg"; export interface Props extends ButtonProps { - onClick: () => Promise - forceLoading?: boolean - errorPlacement?: import("react-laag/dist/PlacementType").PlacementType - children: ReactNode + onClick: () => Promise; + forceLoading?: boolean; + errorPlacement?: import("react-laag/dist/PlacementType").PlacementType; + children: ReactNode; } export default function AsyncButton(props: Props) { - const [isAwaitingPromise, setIsAwaitingPromise] = useState(false) - const isLoading = isAwaitingPromise || props.forceLoading - const [errorMessage, setErrorMessage] = useState("") - const clickHandler = props.onClick + const [isAwaitingPromise, setIsAwaitingPromise] = useState(false); + const isLoading = isAwaitingPromise || props.forceLoading; + const [errorMessage, setErrorMessage] = useState(""); + const clickHandler = props.onClick; const onClick = useCallback(() => { if (!isLoading) { - setIsAwaitingPromise(true) - setErrorMessage("") + setIsAwaitingPromise(true); + setErrorMessage(""); - const promise = clickHandler() + const promise = clickHandler(); if (promise instanceof Promise) { - promise.catch(error => { - console.error("AsyncButton caught error", error) - setErrorMessage(error.message || error.toString()) - }).finally(() => { - setIsAwaitingPromise(false) - }) + promise + .catch((error) => { + console.error("AsyncButton caught error", error); + setErrorMessage(error.message || error.toString()); + }) + .finally(() => { + setIsAwaitingPromise(false); + }); } else { - console.error("AsyncButton onClick() must return a promise, but instead it returned", promise) - setIsAwaitingPromise(false) + console.error( + "AsyncButton onClick() must return a promise, but instead it returned", + promise, + ); + setIsAwaitingPromise(false); } } - }, [isLoading, clickHandler]) + }, [isLoading, clickHandler]); const { triggerProps, layerProps, arrowProps, renderLayer } = useLayer({ isOpen: errorMessage !== "", onOutsideClick: () => setErrorMessage(""), placement: props.errorPlacement ?? "top-center", auto: true, triggerOffset: 8, - }) + }); - return + {renderLayer( + + {errorMessage && ( + +
    {errorMessage}
    + +
    + )} +
    , + )} + + ); } diff --git a/frontend/src/components/Breadcrumbs.tsx b/frontend/src/components/Breadcrumbs.tsx index e4cb66c79..26a2b4153 100644 --- a/frontend/src/components/Breadcrumbs.tsx +++ b/frontend/src/components/Breadcrumbs.tsx @@ -1,38 +1,49 @@ -import type { ReactNode } from "react" +import type { ReactNode } from "react"; -import Link from "next/link" +import Link from "next/link"; -import classNames from "classnames" +import classNames from "classnames"; -import styles from "./Breadcrumbs.module.scss" +import styles from "./Breadcrumbs.module.scss"; export interface Props { pages: { - label: ReactNode - href?: string - }[] - className?: string + label: ReactNode; + href?: string; + }[]; + className?: string; } export default function Breadcrumbs({ pages, className }: Props) { // https://www.w3.org/TR/wai-aria-practices/examples/breadcrumb/index.html return ( - - ) + ); } diff --git a/frontend/src/components/Nav/Search.tsx b/frontend/src/components/Nav/Search.tsx index 1e4de772d..8090e8c4f 100644 --- a/frontend/src/components/Nav/Search.tsx +++ b/frontend/src/components/Nav/Search.tsx @@ -1,77 +1,79 @@ -import { useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState } from "react"; -import Image from "next/image" -import { useRouter } from "next/navigation" +import Image from "next/image"; +import { useRouter } from "next/navigation"; -import { SearchIcon } from "@primer/octicons-react" -import classNames from "classnames" -import { useCombobox } from "downshift" -import { useLayer } from "react-laag" +import { SearchIcon } from "@primer/octicons-react"; +import classNames from "classnames"; +import { useCombobox } from "downshift"; +import { useLayer } from "react-laag"; -import * as api from "@/lib/api" -import { scratchUrl, userAvatarUrl } from "@/lib/api/urls" +import * as api from "@/lib/api"; +import { scratchUrl, userAvatarUrl } from "@/lib/api/urls"; -import LoadingSpinner from "../loading.svg" -import PlatformLink from "../PlatformLink" -import AnonymousFrogAvatar from "../user/AnonymousFrog" -import verticalMenuStyles from "../VerticalMenu.module.scss" // eslint-disable-line css-modules/no-unused-class +import LoadingSpinner from "../loading.svg"; +import PlatformLink from "../PlatformLink"; +import AnonymousFrogAvatar from "../user/AnonymousFrog"; +import verticalMenuStyles from "../VerticalMenu.module.scss"; // eslint-disable-line css-modules/no-unused-class -import styles from "./Search.module.scss" +import styles from "./Search.module.scss"; function MountedSearch({ className }: { className?: string }) { - const [query, setQuery] = useState("") - const [isFocused, setIsFocused] = useState(false) - const [isLoading, setIsLoading] = useState(false) - const [debouncedTimeout, setDebouncedTimeout] = useState() - const [searchItems, setSearchItems] = useState([]) + const [query, setQuery] = useState(""); + const [isFocused, setIsFocused] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [debouncedTimeout, setDebouncedTimeout] = useState(); + const [searchItems, setSearchItems] = useState([]); - const items = query.length > 0 ? searchItems : [] + const items = query.length > 0 ? searchItems : []; const close = () => { - console.info(" close") - setInputValue("") - setQuery("") - setIsFocused(false) - } + console.info(" close"); + setInputValue(""); + setQuery(""); + setIsFocused(false); + }; + + const { isOpen, getMenuProps, getInputProps, getItemProps, setInputValue } = + useCombobox({ + items, + isOpen: + (isFocused || !!query) && + query.length > 0 && + !(isLoading && items.length === 0), + itemToString(item) { + return item.name; + }, + onInputValueChange({ inputValue }) { + clearTimeout(debouncedTimeout); + setQuery(inputValue); + + if (inputValue.length === 0) { + setSearchItems([]); + setIsLoading(false); + return; + } - const { - isOpen, - getMenuProps, - getInputProps, - getItemProps, - setInputValue, - } = useCombobox({ - items, - isOpen: (isFocused || !!query) && query.length > 0 && !(isLoading && items.length === 0), - itemToString(item) { - return item.name - }, - onInputValueChange({ inputValue }) { - clearTimeout(debouncedTimeout) - setQuery(inputValue) - - if (inputValue.length === 0) { - setSearchItems([]) - setIsLoading(false) - return - } - - // Use a timeout to avoid spamming the API with requests - setIsLoading(true) - setDebouncedTimeout(setTimeout(async () => { - const resp = await api.get(`/scratch?search=${inputValue}&page_size=5`) - setSearchItems(resp.results) - setIsLoading(false) - }, 200)) - }, - onSelectedItemChange({ selectedItem }) { - if (selectedItem) { - console.info(" onSelectedItemChange") - close() - router.push(scratchUrl(selectedItem)) - } - }, - }) + // Use a timeout to avoid spamming the API with requests + setIsLoading(true); + setDebouncedTimeout( + setTimeout(async () => { + const resp = await api.get( + `/scratch?search=${inputValue}&page_size=5`, + ); + setSearchItems(resp.results); + setIsLoading(false); + }, 200), + ); + }, + onSelectedItemChange({ selectedItem }) { + if (selectedItem) { + console.info(" onSelectedItemChange"); + close(); + router.push(scratchUrl(selectedItem)); + } + }, + }); const { renderLayer, triggerProps, layerProps, triggerBounds } = useLayer({ isOpen, @@ -83,112 +85,140 @@ function MountedSearch({ className }: { className?: string }) { triggerOffset: 0, containerOffset: 16, onOutsideClick() { - console.info(" onOutsideClick") - close() + console.info(" onOutsideClick"); + close(); }, - }) + }); - const router = useRouter() + const router = useRouter(); - const lastWidthRef = useRef(0) + const lastWidthRef = useRef(0); if (triggerBounds) { - lastWidthRef.current = triggerBounds.width + lastWidthRef.current = triggerBounds.width; } - return
    { - if (evt.key === "Enter") { - evt.stopPropagation() - evt.preventDefault() - - if (searchItems.length > 0) { - console.info(" Enter pressed") - close() - router.push(scratchUrl(searchItems[0])) + return ( +
    { + if (evt.key === "Enter") { + evt.stopPropagation(); + evt.preventDefault(); + + if (searchItems.length > 0) { + console.info(" Enter pressed"); + close(); + router.push(scratchUrl(searchItems[0])); + } } - } - }} - > - - setIsFocused(true)} - onClick={() => setIsFocused(true)} - /> - {isLoading && isFocused && } - {renderLayer( - - )} -
    + type="text" + placeholder="Search scratches" + spellCheck={false} + onFocus={() => setIsFocused(true)} + onClick={() => setIsFocused(true)} + /> + {isLoading && isFocused && ( + + )} + {renderLayer( + , + )} +
    + ); } export default function Search({ className }: { className?: string }) { - const [isMounted, setIsMounted] = useState(false) - useEffect(() => setIsMounted(true), []) + const [isMounted, setIsMounted] = useState(false); + useEffect(() => setIsMounted(true), []); if (!isMounted) { - return null + return null; } - return + return ; } diff --git a/frontend/src/components/Nav/UserMenuItems.tsx b/frontend/src/components/Nav/UserMenuItems.tsx index 76a978d97..377fb11a9 100644 --- a/frontend/src/components/Nav/UserMenuItems.tsx +++ b/frontend/src/components/Nav/UserMenuItems.tsx @@ -1,46 +1,48 @@ -import { mutate } from "swr" +import { mutate } from "swr"; -import * as api from "@/lib/api" +import * as api from "@/lib/api"; -import GitHubLoginButton from "../GitHubLoginButton" -import { MenuItem, ButtonItem, LinkItem } from "../VerticalMenu" +import GitHubLoginButton from "../GitHubLoginButton"; +import { MenuItem, ButtonItem, LinkItem } from "../VerticalMenu"; -import styles from "./UserMenuItems.module.scss" +import styles from "./UserMenuItems.module.scss"; export default function UserMenuItems() { - const user = api.useThisUser() + const user = api.useThisUser(); if (api.isAnonUser(user)) { - return <> + return ( + <> + +
    + Sign in now to keep track of your scratches. +
    +
    + + + + + ); + } + + return ( + <>
    - Sign in now to keep track of your scratches. + Signed in as {user.username}
    - - - + Your profile +
    + {user.is_admin && Admin} + { + const user = await api.post("/user", {}); + await mutate("/user", user); + }} + > + Sign out + - } - - return <> - -
    - Signed in as {user.username} -
    -
    - - Your profile - -
    - {user.is_admin && Admin} - { - const user = await api.post("/user", {}) - await mutate("/user", user) - }} - > - Sign out - - + ); } diff --git a/frontend/src/components/Nav/index.tsx b/frontend/src/components/Nav/index.tsx index bc5224b6c..36b628548 100644 --- a/frontend/src/components/Nav/index.tsx +++ b/frontend/src/components/Nav/index.tsx @@ -1,3 +1,3 @@ -import Nav from "./Nav" +import Nav from "./Nav"; -export default Nav +export default Nav; diff --git a/frontend/src/components/NumberInput.tsx b/frontend/src/components/NumberInput.tsx index 3bb414930..2378b4a1e 100644 --- a/frontend/src/components/NumberInput.tsx +++ b/frontend/src/components/NumberInput.tsx @@ -1,57 +1,68 @@ -import { useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState } from "react"; -import classNames from "classnames" +import classNames from "classnames"; -import styles from "./NumberInput.module.scss" +import styles from "./NumberInput.module.scss"; export type Props = { - value?: number - onChange?: (value: number) => void - stringValue?: string - disabled?: boolean -} + value?: number; + onChange?: (value: number) => void; + stringValue?: string; + disabled?: boolean; +}; -export default function NumberInput({ value, onChange, stringValue, disabled }: Props) { - const [isEditing, setIsEditing] = useState(false) - const editableRef = useRef() +export default function NumberInput({ + value, + onChange, + stringValue, + disabled, +}: Props) { + const [isEditing, setIsEditing] = useState(false); + const editableRef = useRef(); useEffect(() => { - const el = editableRef.current + const el = editableRef.current; if (el) { - const range = document.createRange() - range.selectNodeContents(el) - const sel = window.getSelection() - sel.removeAllRanges() - sel.addRange(range) + const range = document.createRange(); + range.selectNodeContents(el); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); } - }, [isEditing]) - - return setIsEditing(true)} - onBlur={evt => { - if (Number.isNaN(+evt.currentTarget.textContent)) { - evt.currentTarget.textContent = `${value}` // this should never happen, as the user is not allowed to type non-digits - } - onChange(+evt.currentTarget.textContent) - setIsEditing(false) - }} - onKeyPress={evt => { - const isValidKey = evt.key === "." || !Number.isNaN(+evt.key) - if (!isValidKey || disabled) { - evt.preventDefault() - } - - if (evt.key === "Enter") { - evt.currentTarget.blur() // submit - } - }} - > - {isEditing ? editableRef.current.textContent : (stringValue ?? value)} - + }, [isEditing]); + + return ( + setIsEditing(true)} + onBlur={(evt) => { + if (Number.isNaN(+evt.currentTarget.textContent)) { + evt.currentTarget.textContent = `${value}`; // this should never happen, as the user is not allowed to type non-digits + } + onChange(+evt.currentTarget.textContent); + setIsEditing(false); + }} + onKeyPress={(evt) => { + const isValidKey = evt.key === "." || !Number.isNaN(+evt.key); + if (!isValidKey || disabled) { + evt.preventDefault(); + } + + if (evt.key === "Enter") { + evt.currentTarget.blur(); // submit + } + }} + > + {isEditing + ? editableRef.current.textContent + : (stringValue ?? value)} + + ); } diff --git a/frontend/src/components/PlatformLink.tsx b/frontend/src/components/PlatformLink.tsx index 04d452c36..5a5f94bf5 100644 --- a/frontend/src/components/PlatformLink.tsx +++ b/frontend/src/components/PlatformLink.tsx @@ -1,20 +1,26 @@ -import Link from "next/link" +import Link from "next/link"; -import type * as api from "@/lib/api" -import { platformUrl } from "@/lib/api/urls" +import type * as api from "@/lib/api"; +import { platformUrl } from "@/lib/api/urls"; -import { platformIcon } from "./PlatformSelect/PlatformIcon" +import { platformIcon } from "./PlatformSelect/PlatformIcon"; export type Props = { - scratch: api.TerseScratch - size: number - className?: string -} + scratch: api.TerseScratch; + size: number; + className?: string; +}; export default function PlatformLink(props: Props) { - const Icon = platformIcon(props.scratch.platform) + const Icon = platformIcon(props.scratch.platform); - return - - + return ( + + + + ); } diff --git a/frontend/src/components/PlatformSelect/PlatformIcon.tsx b/frontend/src/components/PlatformSelect/PlatformIcon.tsx index b25f18f1c..442b1221a 100644 --- a/frontend/src/components/PlatformSelect/PlatformIcon.tsx +++ b/frontend/src/components/PlatformSelect/PlatformIcon.tsx @@ -1,69 +1,67 @@ -import Link from "next/link" +import Link from "next/link"; -import { platformUrl } from "@/lib/api/urls" +import { platformUrl } from "@/lib/api/urls"; -import LogoDreamcast from "./dreamcast.svg" -import LogoGBA from "./gba.svg" -import LogoGCWii from "./gc_wii.svg" -import LogoIRIX from "./irix.svg" -import LogoMacOSX from "./macosx.svg" -import LogoMSDOS from "./msdos.svg" -import LogoN3DS from "./n3ds.svg" -import LogoN64 from "./n64.svg" -import LogoNDS from "./nds.svg" -import LogoPS1 from "./ps1.svg" -import LogoPS2 from "./ps2.svg" -import LogoPSP from "./psp.svg" -import LogoSaturn from "./saturn.svg" -import LogoSwitch from "./switch.svg" -import UnknownIcon from "./unknown.svg" -import LogoWin32 from "./win32.svg" +import LogoDreamcast from "./dreamcast.svg"; +import LogoGBA from "./gba.svg"; +import LogoGCWii from "./gc_wii.svg"; +import LogoIRIX from "./irix.svg"; +import LogoMacOSX from "./macosx.svg"; +import LogoMSDOS from "./msdos.svg"; +import LogoN3DS from "./n3ds.svg"; +import LogoN64 from "./n64.svg"; +import LogoNDS from "./nds.svg"; +import LogoPS1 from "./ps1.svg"; +import LogoPS2 from "./ps2.svg"; +import LogoPSP from "./psp.svg"; +import LogoSaturn from "./saturn.svg"; +import LogoSwitch from "./switch.svg"; +import UnknownIcon from "./unknown.svg"; +import LogoWin32 from "./win32.svg"; /** In release-date order */ const ICONS = { - "msdos": LogoMSDOS, - "irix": LogoIRIX, - "win32": LogoWin32, - "macosx": LogoMacOSX, - "n64": LogoN64, - "gba": LogoGBA, - "gc_wii": LogoGCWii, - "nds_arm9": LogoNDS, - "ps1": LogoPS1, - "ps2": LogoPS2, - "psp": LogoPSP, - "n3ds": LogoN3DS, - "switch": LogoSwitch, - "saturn": LogoSaturn, - "dreamcast": LogoDreamcast, -} + msdos: LogoMSDOS, + irix: LogoIRIX, + win32: LogoWin32, + macosx: LogoMacOSX, + n64: LogoN64, + gba: LogoGBA, + gc_wii: LogoGCWii, + nds_arm9: LogoNDS, + ps1: LogoPS1, + ps2: LogoPS2, + psp: LogoPSP, + n3ds: LogoN3DS, + switch: LogoSwitch, + saturn: LogoSaturn, + dreamcast: LogoDreamcast, +}; -export const PLATFORMS = Object.keys(ICONS) +export const PLATFORMS = Object.keys(ICONS); export type Props = { - platform: string - className?: string - clickable?: boolean - size?: string | number -} + platform: string; + className?: string; + clickable?: boolean; + size?: string | number; +}; export function platformIcon(platform: string) { - return ICONS[platform as keyof typeof ICONS] || UnknownIcon + return ICONS[platform as keyof typeof ICONS] || UnknownIcon; } export function PlatformIcon({ platform, className, clickable, size }: Props) { - const Icon = platformIcon(platform) - const url = platformUrl(platform) + const Icon = platformIcon(platform); + const url = platformUrl(platform); if (clickable) { return ( - ) + ); } else { - return ( - - ) + return ; } } diff --git a/frontend/src/components/PlatformSelect/PlatformName.tsx b/frontend/src/components/PlatformSelect/PlatformName.tsx index de3fbe137..1069bd772 100644 --- a/frontend/src/components/PlatformSelect/PlatformName.tsx +++ b/frontend/src/components/PlatformSelect/PlatformName.tsx @@ -1,19 +1,22 @@ -import Link from "next/link" +import Link from "next/link"; -import useSWRImmutable from "swr/immutable" +import useSWRImmutable from "swr/immutable"; -import * as api from "@/lib/api" +import * as api from "@/lib/api"; export type Props = { - platform: string -} + platform: string; +}; export default function PlatformName({ platform }: Props) { - const { data } = useSWRImmutable(`/platform/${platform}`, api.get) + const { data } = useSWRImmutable( + `/platform/${platform}`, + api.get, + ); - return <> - - {data?.name ?? platform} - - + return ( + <> + {data?.name ?? platform} + + ); } diff --git a/frontend/src/components/PlatformSelect/PlatformSelect.tsx b/frontend/src/components/PlatformSelect/PlatformSelect.tsx index 1babdf177..18b497e50 100644 --- a/frontend/src/components/PlatformSelect/PlatformSelect.tsx +++ b/frontend/src/components/PlatformSelect/PlatformSelect.tsx @@ -1,35 +1,49 @@ -import classNames from "classnames" +import classNames from "classnames"; -import { PlatformIcon } from "./PlatformIcon" -import styles from "./PlatformSelect.module.scss" +import { PlatformIcon } from "./PlatformIcon"; +import styles from "./PlatformSelect.module.scss"; export type Props = { platforms: { [key: string]: { - name: string - description: string - } - } - value: string - className?: string - onChange: (value: string) => void -} + name: string; + description: string; + }; + }; + value: string; + className?: string; + onChange: (value: string) => void; +}; -export default function PlatformSelect({ platforms, value, onChange, className }: Props) { - if (!value) - onChange("n64") +export default function PlatformSelect({ + platforms, + value, + onChange, + className, +}: Props) { + if (!value) onChange("n64"); - return
      - {Object.entries(platforms).map(([key, platform]) =>
    • onChange(key)} - > - -
      -
      {platform.name}
      -
      {platform.description}
      -
      -
    • )} -
    + return ( +
      + {Object.entries(platforms).map(([key, platform]) => ( +
    • onChange(key)} + > + +
      +
      + {platform.name} +
      +
      + {platform.description} +
      +
      +
    • + ))} +
    + ); } diff --git a/frontend/src/components/PlatformSelect/ScrollingPlatformIcons.tsx b/frontend/src/components/PlatformSelect/ScrollingPlatformIcons.tsx index 2c6dea262..fb4add9c8 100644 --- a/frontend/src/components/PlatformSelect/ScrollingPlatformIcons.tsx +++ b/frontend/src/components/PlatformSelect/ScrollingPlatformIcons.tsx @@ -1,17 +1,27 @@ -import classNames from "classnames" +import classNames from "classnames"; -import { PlatformIcon, PLATFORMS } from "./PlatformIcon" -import styles from "./ScrollingPlatformIcons.module.scss" +import { PlatformIcon, PLATFORMS } from "./PlatformIcon"; +import styles from "./ScrollingPlatformIcons.module.scss"; function SingleSet() { - return
    - {PLATFORMS.map(platform => )} -
    + return ( +
    + {PLATFORMS.map((platform) => ( + + ))} +
    + ); } export default function ScrollingPlatformIcons() { - return
    - - -
    + return ( +
    + + +
    + ); } diff --git a/frontend/src/components/PlatformSelect/index.ts b/frontend/src/components/PlatformSelect/index.ts index 941f84ad2..11d5d4fdf 100644 --- a/frontend/src/components/PlatformSelect/index.ts +++ b/frontend/src/components/PlatformSelect/index.ts @@ -1,4 +1,6 @@ -import PlatformSelect, { type Props as PlatformSelectProps } from "./PlatformSelect" +import PlatformSelect, { + type Props as PlatformSelectProps, +} from "./PlatformSelect"; -export type Props = PlatformSelectProps -export default PlatformSelect +export type Props = PlatformSelectProps; +export default PlatformSelect; diff --git a/frontend/src/components/PresetList.tsx b/frontend/src/components/PresetList.tsx index 40402bc3d..4c6466866 100644 --- a/frontend/src/components/PresetList.tsx +++ b/frontend/src/components/PresetList.tsx @@ -1,73 +1,109 @@ -"use client" +"use client"; -import type { ReactNode, JSX } from "react" +import type { ReactNode, JSX } from "react"; -import Link from "next/link" +import Link from "next/link"; -import classNames from "classnames" +import classNames from "classnames"; -import AsyncButton from "@/components/AsyncButton" -import Button from "@/components/Button" -import LoadingSpinner from "@/components/loading.svg" -import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon" -import { type Preset, usePaginated } from "@/lib/api" -import { presetUrl } from "@/lib/api/urls" -import getTranslation from "@/lib/i18n/translate" +import AsyncButton from "@/components/AsyncButton"; +import Button from "@/components/Button"; +import LoadingSpinner from "@/components/loading.svg"; +import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon"; +import { type Preset, usePaginated } from "@/lib/api"; +import { presetUrl } from "@/lib/api/urls"; +import getTranslation from "@/lib/i18n/translate"; export interface Props { - url?: string - className?: string - item?: ({ preset }: { preset: Preset }) => JSX.Element - emptyButtonLabel?: ReactNode + url?: string; + className?: string; + item?: ({ preset }: { preset: Preset }) => JSX.Element; + emptyButtonLabel?: ReactNode; } -export function PresetList({ url, className, item, emptyButtonLabel }: Props): JSX.Element { - const { results, isLoading, hasNext, loadNext } = usePaginated(url || "/preset") +export function PresetList({ + url, + className, + item, + emptyButtonLabel, +}: Props): JSX.Element { + const { results, isLoading, hasNext, loadNext } = usePaginated( + url || "/preset", + ); if (results.length === 0 && isLoading) { - return
    - - Just a moment... -
    + return ( +
    + + Just a moment... +
    + ); } - const Item = item ?? PresetItem + const Item = item ?? PresetItem; return ( -
      - {results.map(preset => ( +
        + {results.map((preset) => ( ))} - {results.length === 0 && emptyButtonLabel &&
      • - - - - - -
      • } - {hasNext &&
      • - - Show more - -
      • } + {results.length === 0 && emptyButtonLabel && ( +
      • + + + +
      • + )} + {hasNext && ( +
      • + Show more +
      • + )}
      - ) + ); } -export function PresetItem({ preset, hideIcon }: { preset: Preset, hideIcon?: boolean }): JSX.Element { - const compilersTranslation = getTranslation("compilers") - const compilerName = compilersTranslation.t(preset.compiler) +export function PresetItem({ + preset, + hideIcon, +}: { preset: Preset; hideIcon?: boolean }): JSX.Element { + const compilersTranslation = getTranslation("compilers"); + const compilerName = compilersTranslation.t(preset.compiler); return (

      {compilerName}

      - ) + ); } diff --git a/frontend/src/components/ScoreBadge.tsx b/frontend/src/components/ScoreBadge.tsx index d72353fe6..4688774c2 100644 --- a/frontend/src/components/ScoreBadge.tsx +++ b/frontend/src/components/ScoreBadge.tsx @@ -1,76 +1,97 @@ -import { AlertIcon, CheckIcon } from "@primer/octicons-react" -import classNames from "classnames" +import { AlertIcon, CheckIcon } from "@primer/octicons-react"; +import classNames from "classnames"; -import styles from "./ScoreBadge.module.scss" +import styles from "./ScoreBadge.module.scss"; export function calculateScorePercent(score: number, maxScore: number): number { if (score > maxScore) { - return 0 + return 0; } if (maxScore === 0) { - return 0 + return 0; } - return ((1 - (score / maxScore)) * 100) + return (1 - score / maxScore) * 100; } export function percentToString(percent: number): string { // If the percent is an integer, don't show the decimal if (Math.floor(percent * 100) / 100 === Math.floor(percent)) { - return `${Math.floor(percent)}%` + return `${Math.floor(percent)}%`; } // If percent is between 99.99 and 100 exclusive, always round down if (99.99 < percent && percent < 100) { - return "99.99%" + return "99.99%"; } - return `${percent.toFixed(2)}%` + return `${percent.toFixed(2)}%`; } -export function getScoreText(score: number, maxScore: number, matchOverride: boolean): string { +export function getScoreText( + score: number, + maxScore: number, + matchOverride: boolean, +): string { if (score === -1) { - return "No score available" + return "No score available"; } else if (score === 0) { - return "0 (100%) 🎊" + return "0 (100%) 🎊"; } else if (matchOverride) { - return `${score} (100%) 🎊 (override)` + return `${score} (100%) 🎊 (override)`; } else { - const percent = calculateScorePercent(score, maxScore) + const percent = calculateScorePercent(score, maxScore); - return `${score} (${percentToString(percent)})` + return `${score} (${percentToString(percent)})`; } } export function getScoreAsFraction(score: number, maxScore: number): string { if (score === -1) { - return `???/${maxScore}` + return `???/${maxScore}`; } else { - return `${maxScore - score}/${maxScore}` + return `${maxScore - score}/${maxScore}`; } } export type Props = { - score: number - maxScore: number - matchOverride: boolean - compiledSuccessfully: boolean -} + score: number; + maxScore: number; + matchOverride: boolean; + compiledSuccessfully: boolean; +}; -export default function ScoreBadge({ score, maxScore, matchOverride, compiledSuccessfully }: Props) { +export default function ScoreBadge({ + score, + maxScore, + matchOverride, + compiledSuccessfully, +}: Props) { if (!compiledSuccessfully || score === -1) { - return
      - -
      + return ( +
      + +
      + ); } else if (score === 0) { - return
      - -
      + return ( +
      + +
      + ); } else { - const text = getScoreText(score, maxScore, matchOverride) - const title = getScoreAsFraction(score, maxScore) + const text = getScoreText(score, maxScore, matchOverride); + const title = getScoreAsFraction(score, maxScore); - return
      - {text} -
      + return ( +
      + {text} +
      + ); } } diff --git a/frontend/src/components/Scratch/Scratch.tsx b/frontend/src/components/Scratch/Scratch.tsx index d2b50d511..ec3490474 100644 --- a/frontend/src/components/Scratch/Scratch.tsx +++ b/frontend/src/components/Scratch/Scratch.tsx @@ -1,32 +1,41 @@ -import { useEffect, useReducer, useRef, useState } from "react" +import { useEffect, useReducer, useRef, useState } from "react"; -import type { EditorView } from "@codemirror/view" -import { vim } from "@replit/codemirror-vim" +import type { EditorView } from "@codemirror/view"; +import { vim } from "@replit/codemirror-vim"; -import * as api from "@/lib/api" -import basicSetup from "@/lib/codemirror/basic-setup" -import { cpp } from "@/lib/codemirror/cpp" -import useCompareExtension from "@/lib/codemirror/useCompareExtension" -import { useSize } from "@/lib/hooks" -import { useAutoRecompileSetting, useAutoRecompileDelaySetting, useLanguageServerEnabled, useVimModeEnabled, useMatchProgressBarEnabled } from "@/lib/settings" +import * as api from "@/lib/api"; +import basicSetup from "@/lib/codemirror/basic-setup"; +import { cpp } from "@/lib/codemirror/cpp"; +import useCompareExtension from "@/lib/codemirror/useCompareExtension"; +import { useSize } from "@/lib/hooks"; +import { + useAutoRecompileSetting, + useAutoRecompileDelaySetting, + useLanguageServerEnabled, + useVimModeEnabled, + useMatchProgressBarEnabled, +} from "@/lib/settings"; -import CompilerOpts from "../compiler/CompilerOpts" -import CustomLayout, { activateTabInLayout, type Layout } from "../CustomLayout" -import CompilationPanel from "../Diff/CompilationPanel" -import CodeMirror from "../Editor/CodeMirror" -import ErrorBoundary from "../ErrorBoundary" -import ScoreBadge, { calculateScorePercent } from "../ScoreBadge" -import { ScrollContext } from "../ScrollContext" -import { Tab, TabCloseButton } from "../Tabs" +import CompilerOpts from "../compiler/CompilerOpts"; +import CustomLayout, { + activateTabInLayout, + type Layout, +} from "../CustomLayout"; +import CompilationPanel from "../Diff/CompilationPanel"; +import CodeMirror from "../Editor/CodeMirror"; +import ErrorBoundary from "../ErrorBoundary"; +import ScoreBadge, { calculateScorePercent } from "../ScoreBadge"; +import { ScrollContext } from "../ScrollContext"; +import { Tab, TabCloseButton } from "../Tabs"; -import useLanguageServer from "./hooks/useLanguageServer" -import AboutPanel from "./panels/AboutPanel" -import DecompilationPanel from "./panels/DecompilePanel" -import FamilyPanel from "./panels/FamilyPanel" -import styles from "./Scratch.module.scss" -import ScratchMatchBanner from "./ScratchMatchBanner" -import ScratchProgressBar from "./ScratchProgressBar" -import ScratchToolbar from "./ScratchToolbar" +import useLanguageServer from "./hooks/useLanguageServer"; +import AboutPanel from "./panels/AboutPanel"; +import DecompilationPanel from "./panels/DecompilePanel"; +import FamilyPanel from "./panels/FamilyPanel"; +import styles from "./Scratch.module.scss"; +import ScratchMatchBanner from "./ScratchMatchBanner"; +import ScratchProgressBar from "./ScratchProgressBar"; +import ScratchToolbar from "./ScratchToolbar"; enum TabId { ABOUT = "scratch_about", @@ -62,10 +71,7 @@ const DEFAULT_LAYOUTS: Record<"desktop_2col" | "mobile_2row", Layout> = { kind: "pane", size: 50, activeTab: TabId.DIFF, - tabs: [ - TabId.DIFF, - TabId.DECOMPILATION, - ], + tabs: [TabId.DIFF, TabId.DECOMPILATION], }, ], }, @@ -91,35 +97,31 @@ const DEFAULT_LAYOUTS: Record<"desktop_2col" | "mobile_2row", Layout> = { kind: "pane", size: 50, activeTab: TabId.SOURCE_CODE, - tabs: [ - TabId.SOURCE_CODE, - TabId.CONTEXT, - TabId.OPTIONS, - ], + tabs: [TabId.SOURCE_CODE, TabId.CONTEXT, TabId.OPTIONS], }, ], }, -} +}; -const CODEMIRROR_EXTENSIONS = [ - basicSetup, - cpp(), -] -function getDefaultLayout(width: number, _height: number): keyof typeof DEFAULT_LAYOUTS { +const CODEMIRROR_EXTENSIONS = [basicSetup, cpp()]; +function getDefaultLayout( + width: number, + _height: number, +): keyof typeof DEFAULT_LAYOUTS { if (width > 700) { - return "desktop_2col" + return "desktop_2col"; } - return "mobile_2row" + return "mobile_2row"; } export type Props = { - scratch: Readonly - onChange: (scratch: Partial) => void - parentScratch?: api.Scratch - initialCompilation?: Readonly - offline: boolean -} + scratch: Readonly; + onChange: (scratch: Partial) => void; + parentScratch?: api.Scratch; + initialCompilation?: Readonly; + offline: boolean; +}; export default function Scratch({ scratch, @@ -128,225 +130,322 @@ export default function Scratch({ initialCompilation, offline, }: Props) { - const container = useSize() - const [layout, setLayout] = useState(undefined) - const [layoutName, setLayoutName] = useState(undefined) + const container = useSize(); + const [layout, setLayout] = useState(undefined); + const [layoutName, setLayoutName] = + useState(undefined); - const [autoRecompileSetting] = useAutoRecompileSetting() - const [autoRecompileDelaySetting] = useAutoRecompileDelaySetting() - const [languageServerEnabledSetting] = useLanguageServerEnabled() - const [matchProgressBarEnabledSetting] = useMatchProgressBarEnabled() - const { compilation, isCompiling, isCompilationOld, compile } = api.useCompilation(scratch, autoRecompileSetting, autoRecompileDelaySetting, initialCompilation) - const userIsYou = api.useUserIsYou() - const [selectedSourceLine, setSelectedSourceLine] = useState() - const sourceEditor = useRef() - const contextEditor = useRef() - const [valueVersion, incrementValueVersion] = useReducer(x => x + 1, 0) + const [autoRecompileSetting] = useAutoRecompileSetting(); + const [autoRecompileDelaySetting] = useAutoRecompileDelaySetting(); + const [languageServerEnabledSetting] = useLanguageServerEnabled(); + const [matchProgressBarEnabledSetting] = useMatchProgressBarEnabled(); + const { compilation, isCompiling, isCompilationOld, compile } = + api.useCompilation( + scratch, + autoRecompileSetting, + autoRecompileDelaySetting, + initialCompilation, + ); + const userIsYou = api.useUserIsYou(); + const [selectedSourceLine, setSelectedSourceLine] = useState< + number | null + >(); + const sourceEditor = useRef(); + const contextEditor = useRef(); + const [valueVersion, incrementValueVersion] = useReducer((x) => x + 1, 0); - const [isModified, setIsModified] = useState(false) + const [isModified, setIsModified] = useState(false); const setScratch = (scratch: Partial) => { - onChange(scratch) - setIsModified(true) - } - const [perSaveObj, setPerSaveObj] = useState({}) + onChange(scratch); + setIsModified(true); + }; + const [perSaveObj, setPerSaveObj] = useState({}); const saveCallback = () => { - setPerSaveObj({}) - } + setPerSaveObj({}); + }; - const shouldCompare = !isModified - const sourceCompareExtension = useCompareExtension(sourceEditor, shouldCompare ? parentScratch?.source_code : undefined) - const contextCompareExtension = useCompareExtension(contextEditor, shouldCompare ? parentScratch?.context : undefined) + const shouldCompare = !isModified; + const sourceCompareExtension = useCompareExtension( + sourceEditor, + shouldCompare ? parentScratch?.source_code : undefined, + ); + const contextCompareExtension = useCompareExtension( + contextEditor, + shouldCompare ? parentScratch?.context : undefined, + ); - const [saveSource, saveContext] = useLanguageServer(languageServerEnabledSetting, scratch, sourceEditor, contextEditor) + const [saveSource, saveContext] = useLanguageServer( + languageServerEnabledSetting, + scratch, + sourceEditor, + contextEditor, + ); - const lastGoodScore = useRef(scratch.score) - const lastGoodMaxScore = useRef(scratch.max_score) + const lastGoodScore = useRef(scratch.score); + const lastGoodMaxScore = useRef(scratch.max_score); if (compilation?.success) { - lastGoodScore.current = compilation?.diff_output?.current_score - lastGoodMaxScore.current = compilation?.diff_output?.max_score + lastGoodScore.current = compilation?.diff_output?.current_score; + lastGoodMaxScore.current = compilation?.diff_output?.max_score; } // TODO: CustomLayout should handle adding/removing tabs - const [decompilationTabEnabled, setDecompilationTabEnabled] = useState(false) + const [decompilationTabEnabled, setDecompilationTabEnabled] = + useState(false); useEffect(() => { if (decompilationTabEnabled) { - setLayout(layout => { - const clone = { ...layout } - activateTabInLayout(clone, TabId.DECOMPILATION) - return clone - }) + setLayout((layout) => { + const clone = { ...layout }; + activateTabInLayout(clone, TabId.DECOMPILATION); + return clone; + }); } - }, [decompilationTabEnabled]) + }, [decompilationTabEnabled]); // If the version of the scratch changes, refresh code editors useEffect(() => { - incrementValueVersion() - }, [scratch.slug, scratch.last_updated]) + incrementValueVersion(); + }, [scratch.slug, scratch.last_updated]); - const [useVim] = useVimModeEnabled() - const cmExtensionsSource = [...CODEMIRROR_EXTENSIONS, sourceCompareExtension] - const cmExtensionsContext = [...CODEMIRROR_EXTENSIONS, contextCompareExtension] + const [useVim] = useVimModeEnabled(); + const cmExtensionsSource = [ + ...CODEMIRROR_EXTENSIONS, + sourceCompareExtension, + ]; + const cmExtensionsContext = [ + ...CODEMIRROR_EXTENSIONS, + contextCompareExtension, + ]; if (useVim) { - cmExtensionsSource.push(vim()) - cmExtensionsContext.push(vim()) + cmExtensionsSource.push(vim()); + cmExtensionsContext.push(vim()); } const renderTab = (id: string) => { switch (id as TabId) { - case TabId.ABOUT: - return - - - case TabId.SOURCE_CODE: - return { - sourceEditor.current?.focus?.() - saveContext() - }} - > - { - setScratch({ source_code: value }) - }} - onSelectedLineChange={setSelectedSourceLine} - extensions={cmExtensionsSource} - /> - - case TabId.CONTEXT: - return { - contextEditor.current?.focus?.() - saveSource() - }} - > - { - setScratch({ context: value }) - }} - extensions={cmExtensionsContext} - /> - - case TabId.OPTIONS: - return -
      - setScratch({ diff_label: d })} - - matchOverride={scratch.match_override} - onMatchOverrideChange={m => setScratch({ match_override: m })} - /> -
      -
      - case TabId.DIFF: - return - Compilation - {compilation && } - } - className={styles.diffTab} - > - {compilation && } - - case TabId.DECOMPILATION: - return decompilationTabEnabled && - Decompilation - setDecompilationTabEnabled(false)} /> - } - > - {() => } - - case TabId.FAMILY: - return - {() => } - - default: - return + case TabId.ABOUT: + return ( + + + + ); + case TabId.SOURCE_CODE: + return ( + { + sourceEditor.current?.focus?.(); + saveContext(); + }} + > + { + setScratch({ source_code: value }); + }} + onSelectedLineChange={setSelectedSourceLine} + extensions={cmExtensionsSource} + /> + + ); + case TabId.CONTEXT: + return ( + { + contextEditor.current?.focus?.(); + saveSource(); + }} + > + { + setScratch({ context: value }); + }} + extensions={cmExtensionsContext} + /> + + ); + case TabId.OPTIONS: + return ( + +
      + + setScratch({ diff_label: d }) + } + matchOverride={scratch.match_override} + onMatchOverrideChange={(m) => + setScratch({ match_override: m }) + } + /> +
      +
      + ); + case TabId.DIFF: + return ( + + Compilation + {compilation && ( + + )} + + } + className={styles.diffTab} + > + {compilation && ( + + )} + + ); + case TabId.DECOMPILATION: + return ( + decompilationTabEnabled && ( + + Decompilation + + setDecompilationTabEnabled(false) + } + /> + + } + > + {() => } + + ) + ); + case TabId.FAMILY: + return ( + + {() => } + + ); + default: + return ; } - } + }; if (container.width) { - const preferredLayout = getDefaultLayout(container.width, container.height) + const preferredLayout = getDefaultLayout( + container.width, + container.height, + ); if (layoutName !== preferredLayout) { - setLayoutName(preferredLayout) - setLayout(DEFAULT_LAYOUTS[preferredLayout]) + setLayoutName(preferredLayout); + setLayout(DEFAULT_LAYOUTS[preferredLayout]); } } - const offlineOverlay = ( - offline ? <> + const offlineOverlay = offline ? ( + <>
      -

      The scratch editor is in offline mode. We're attempting to reconnect to the backend – as long as this tab is open, your work is safe.

      +

      + The scratch editor is in offline mode. We're attempting to + reconnect to the backend – as long as this tab is open, your + work is safe. +

      - : <> - ) + ) : ( + <> + ); - const matchPercent = calculateScorePercent(lastGoodScore.current, lastGoodMaxScore.current) + const matchPercent = calculateScorePercent( + lastGoodScore.current, + lastGoodMaxScore.current, + ); - return
      - - - - - - {matchProgressBarEnabledSetting &&
      } -
      - - {layout && - + + + + + - } - - {offlineOverlay} -
      + {matchProgressBarEnabledSetting && ( +
      + +
      + )} + + + {layout && ( + + + + )} + + {offlineOverlay} +
    + ); } diff --git a/frontend/src/components/Scratch/ScratchMatchBanner.tsx b/frontend/src/components/Scratch/ScratchMatchBanner.tsx index 4bfba5e38..8075be566 100644 --- a/frontend/src/components/Scratch/ScratchMatchBanner.tsx +++ b/frontend/src/components/Scratch/ScratchMatchBanner.tsx @@ -1,34 +1,38 @@ -import Link from "next/link" +import Link from "next/link"; -import useSWR from "swr" +import useSWR from "swr"; -import * as api from "@/lib/api" -import { scratchUrl } from "@/lib/api/urls" +import * as api from "@/lib/api"; +import { scratchUrl } from "@/lib/api/urls"; -import DismissableBanner from "../DismissableBanner" +import DismissableBanner from "../DismissableBanner"; -export default function ScratchMatchBanner({ scratch }: { scratch: api.TerseScratch }) { - const userIsYou = api.useUserIsYou() - const { data, error } = useSWR(`${scratchUrl(scratch)}/family`, api.get, { - refreshInterval: 60 * 1000, // 1 minute - }) +export default function ScratchMatchBanner({ + scratch, +}: { scratch: api.TerseScratch }) { + const userIsYou = api.useUserIsYou(); + const { data, error } = useSWR( + `${scratchUrl(scratch)}/family`, + api.get, + { + refreshInterval: 60 * 1000, // 1 minute + }, + ); // Consciously not including match_override here, since it's not really banner-worthy - const match = data?.find(s => s.score === 0 && s.slug !== scratch.slug) + const match = data?.find((s) => s.score === 0 && s.slug !== scratch.slug); - if (error) - throw error + if (error) throw error; - if (scratch.score === 0 || !match) - return null + if (scratch.score === 0 || !match) return null; - let message = "This function has been matched" - if (userIsYou(match.owner)) - message += " by you, elsewhere" - else if (match.owner) - message += ` by ${match.owner.username}` + let message = "This function has been matched"; + if (userIsYou(match.owner)) message += " by you, elsewhere"; + else if (match.owner) message += ` by ${match.owner.username}`; - return - {message}. View match - + return ( + + {message}. View match + + ); } diff --git a/frontend/src/components/Scratch/ScratchProgressBar.tsx b/frontend/src/components/Scratch/ScratchProgressBar.tsx index e2c978dbd..3a48e61f1 100644 --- a/frontend/src/components/Scratch/ScratchProgressBar.tsx +++ b/frontend/src/components/Scratch/ScratchProgressBar.tsx @@ -1,12 +1,19 @@ -import * as Progress from "@radix-ui/react-progress" +import * as Progress from "@radix-ui/react-progress"; -import styles from "./ScratchProgressbar.module.scss" +import styles from "./ScratchProgressbar.module.scss"; -export default function ScratchProgressBar({ matchPercent }: {matchPercent: number}) { - return - - +export default function ScratchProgressBar({ + matchPercent, +}: { matchPercent: number }) { + return ( + + + + ); } diff --git a/frontend/src/components/Scratch/ScratchToolbar.tsx b/frontend/src/components/Scratch/ScratchToolbar.tsx index 59537b540..d8d537958 100644 --- a/frontend/src/components/Scratch/ScratchToolbar.tsx +++ b/frontend/src/components/Scratch/ScratchToolbar.tsx @@ -1,169 +1,199 @@ -import { useEffect, useRef, useState, type FC } from "react" - -import Link from "next/link" - -import { DownloadIcon, FileIcon, IterationsIcon, RepoForkedIcon, SyncIcon, TrashIcon, UploadIcon } from "@primer/octicons-react" -import classNames from "classnames" -import ContentEditable from "react-contenteditable" - -import TimeAgo from "@/components/TimeAgo" -import * as api from "@/lib/api" -import { scratchUrl } from "@/lib/api/urls" -import { useSize } from "@/lib/hooks" - -import Breadcrumbs from "../Breadcrumbs" -import Nav from "../Nav" -import PlatformLink from "../PlatformLink" -import { SpecialKey, useShortcut } from "../Shortcut" -import UserAvatar from "../user/UserAvatar" - -import useFuzzySaveCallback, { FuzzySaveAction } from "./hooks/useFuzzySaveCallback" -import styles from "./ScratchToolbar.module.scss" - -const ACTIVE_MS = 1000 * 60 +import { useEffect, useRef, useState, type FC } from "react"; + +import Link from "next/link"; + +import { + DownloadIcon, + FileIcon, + IterationsIcon, + RepoForkedIcon, + SyncIcon, + TrashIcon, + UploadIcon, +} from "@primer/octicons-react"; +import classNames from "classnames"; +import ContentEditable from "react-contenteditable"; + +import TimeAgo from "@/components/TimeAgo"; +import * as api from "@/lib/api"; +import { scratchUrl } from "@/lib/api/urls"; +import { useSize } from "@/lib/hooks"; + +import Breadcrumbs from "../Breadcrumbs"; +import Nav from "../Nav"; +import PlatformLink from "../PlatformLink"; +import { SpecialKey, useShortcut } from "../Shortcut"; +import UserAvatar from "../user/UserAvatar"; + +import useFuzzySaveCallback, { + FuzzySaveAction, +} from "./hooks/useFuzzySaveCallback"; +import styles from "./ScratchToolbar.module.scss"; + +const ACTIVE_MS = 1000 * 60; // Prevents XSS function htmlTextOnly(html: string): string { - return html.replace(//g, ">") + return html.replace(//g, ">"); } function exportScratchZip(scratch: api.Scratch) { - const url = api.normalizeUrl(`${scratchUrl(scratch)}/export`) - const a = document.createElement("a") - a.href = url - a.download = `${scratch.name}.zip` - a.click() + const url = api.normalizeUrl(`${scratchUrl(scratch)}/export`); + const a = document.createElement("a"); + a.href = url; + a.download = `${scratch.name}.zip`; + a.click(); } async function deleteScratch(scratch: api.Scratch) { - await api.delete_(scratchUrl(scratch), {}) + await api.delete_(scratchUrl(scratch), {}); - window.location.href = scratch.project ? `/${scratch.project}` : "/" + window.location.href = scratch.project ? `/${scratch.project}` : "/"; } function EditTimeAgo({ date }: { date: string }) { - const isActive = (Date.now() - (new Date(date)).getTime()) < ACTIVE_MS + const isActive = Date.now() - new Date(date).getTime() < ACTIVE_MS; // Rerender after ACTIVE_MS has elapsed if isActive=true - const [, forceUpdate] = useState({}) + const [, forceUpdate] = useState({}); useEffect(() => { if (isActive) { - const interval = setTimeout(() => forceUpdate({}), ACTIVE_MS) - return () => clearInterval(interval) + const interval = setTimeout(() => forceUpdate({}), ACTIVE_MS); + return () => clearInterval(interval); } - }, [isActive]) - - return - {isActive ? <> - Active now - : <> - - } - + }, [isActive]); + + return ( + + {isActive ? ( + <>Active now + ) : ( + <> + + + )} + + ); } -function ScratchName({ name, onChange }: { name: string, onChange?: (name: string) => void }) { - const [isEditing, setEditing] = useState(false) - const editableRef = useRef() +function ScratchName({ + name, + onChange, +}: { name: string; onChange?: (name: string) => void }) { + const [isEditing, setEditing] = useState(false); + const editableRef = useRef(); useEffect(() => { - const el = editableRef.current + const el = editableRef.current; if (el) { - const range = document.createRange() - range.selectNodeContents(el) - const sel = window.getSelection() - sel.removeAllRanges() - sel.addRange(range) + const range = document.createRange(); + range.selectNodeContents(el); + const sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); } - }, [isEditing]) + }, [isEditing]); if (isEditing) { - return { - const name = evt.currentTarget.innerText as string - if (name.length !== 0) - onChange(name) - }} - - onPaste={evt => { - // Only allow pasting text, rather than any HTML. This is redundant due - // to htmlTextOnly but it's nice not to show "" when you paste an image. - - evt.preventDefault() - const text = evt.clipboardData.getData("text") - - // note: we're using document.execCommand, which is deprecated, - // but its no big deal if it doesn't work. - document.execCommand("insertText", false, text) - }} - - onBlur={() => setEditing(false)} - - onKeyDown={evt => { - if (evt.key === "Enter") { - evt.preventDefault() - setEditing(false) - } - }} - /> + return ( + { + const name = evt.currentTarget.innerText as string; + if (name.length !== 0) onChange(name); + }} + onPaste={(evt) => { + // Only allow pasting text, rather than any HTML. This is redundant due + // to htmlTextOnly but it's nice not to show "" when you paste an image. + + evt.preventDefault(); + const text = evt.clipboardData.getData("text"); + + // note: we're using document.execCommand, which is deprecated, + // but its no big deal if it doesn't work. + document.execCommand("insertText", false, text); + }} + onBlur={() => setEditing(false)} + onKeyDown={(evt) => { + if (evt.key === "Enter") { + evt.preventDefault(); + setEditing(false); + } + }} + /> + ); } else { - return
    { - if (onChange) - setEditing(true) - }} - > - {name} -
    + return ( +
    { + if (onChange) setEditing(true); + }} + > + {name} +
    + ); } } -function Actions({ isCompiling, compile, scratch, setScratch, saveCallback, setDecompilationTabEnabled }: Props) { - const userIsYou = api.useUserIsYou() - const forkScratch = api.useForkScratchAndGo(scratch) - const [fuzzySaveAction, fuzzySaveScratch] = useFuzzySaveCallback(scratch, setScratch) - const [isSaving, setIsSaving] = useState(false) - const [isForking, setIsForking] = useState(false) - const canSave = scratch.owner && userIsYou(scratch.owner) - - const platform = api.usePlatform(scratch.platform) - - const fuzzyShortcut = useShortcut([SpecialKey.CTRL_COMMAND, "S"], async () => { - setIsSaving(true) - await fuzzySaveScratch() - setIsSaving(false) - saveCallback() - }) +function Actions({ + isCompiling, + compile, + scratch, + setScratch, + saveCallback, + setDecompilationTabEnabled, +}: Props) { + const userIsYou = api.useUserIsYou(); + const forkScratch = api.useForkScratchAndGo(scratch); + const [fuzzySaveAction, fuzzySaveScratch] = useFuzzySaveCallback( + scratch, + setScratch, + ); + const [isSaving, setIsSaving] = useState(false); + const [isForking, setIsForking] = useState(false); + const canSave = scratch.owner && userIsYou(scratch.owner); + + const platform = api.usePlatform(scratch.platform); + + const fuzzyShortcut = useShortcut( + [SpecialKey.CTRL_COMMAND, "S"], + async () => { + setIsSaving(true); + await fuzzySaveScratch(); + setIsSaving(false); + saveCallback(); + }, + ); const compileShortcut = useShortcut([SpecialKey.CTRL_COMMAND, "J"], () => { - compile() - }) + compile(); + }); - const isAdmin = api.useThisUserIsAdmin() + const isAdmin = api.useThisUserIsAdmin(); return (
    • - - New + + New
    • - {((scratch.owner && userIsYou(scratch.owner)) || isAdmin) &&
    • - -
    • } + + + )}
    • @@ -213,16 +256,16 @@ function Actions({ isCompiling, compile, scratch, setScratch, saveCallback, setD Compile
    • - {platform?.has_decompiler && + {platform?.has_decompiler && (
    • - } + )}
    - ) + ); } enum ActionsLocation { @@ -231,73 +274,104 @@ enum ActionsLocation { } function useActionsLocation(): [ActionsLocation, FC] { - const inNavActions = useSize() + const inNavActions = useSize(); - let location = ActionsLocation.BELOW_NAV + let location = ActionsLocation.BELOW_NAV; - const el = inNavActions.ref.current + const el = inNavActions.ref.current; if (el) { if (el.clientWidth === el.scrollWidth) { - location = ActionsLocation.IN_NAV + location = ActionsLocation.IN_NAV; } } return [ location, - (props: Props) =>
    - -
    , - ] + (props: Props) => ( +
    + +
    + ), + ]; } export type Props = { - isCompiling: boolean - compile: () => Promise - scratch: Readonly - setScratch: (scratch: Partial) => void - saveCallback: () => void - setDecompilationTabEnabled: (enabled: boolean) => void -} + isCompiling: boolean; + compile: () => Promise; + scratch: Readonly; + setScratch: (scratch: Partial) => void; + saveCallback: () => void; + setDecompilationTabEnabled: (enabled: boolean) => void; +}; export default function ScratchToolbar(props: Props) { - const { scratch, setScratch } = props - const userIsYou = api.useUserIsYou() - - const [actionsLocation, InNavActions] = useActionsLocation() - - return <> -
    - - {actionsLocation === ActionsLocation.BELOW_NAV &&
    - -
    } - + const { scratch, setScratch } = props; + const userIsYou = api.useUserIsYou(); + + const [actionsLocation, InNavActions] = useActionsLocation(); + + return ( + <> +
    + + {actionsLocation === ActionsLocation.BELOW_NAV && ( +
    + +
    + )} + + ); } diff --git a/frontend/src/components/Scratch/SortableFamilyList.tsx b/frontend/src/components/Scratch/SortableFamilyList.tsx index 2c3dc0767..25c4334a0 100644 --- a/frontend/src/components/Scratch/SortableFamilyList.tsx +++ b/frontend/src/components/Scratch/SortableFamilyList.tsx @@ -1,27 +1,38 @@ -import { useMemo, useState } from "react" +import { useMemo, useState } from "react"; -import Link from "next/link" +import Link from "next/link"; -import classNames from "classnames" -import useSWR from "swr" +import classNames from "classnames"; +import useSWR from "swr"; -import { get } from "@/lib/api/request" -import type { TerseScratch } from "@/lib/api/types" -import { scratchUrl } from "@/lib/api/urls" +import { get } from "@/lib/api/request"; +import type { TerseScratch } from "@/lib/api/types"; +import { scratchUrl } from "@/lib/api/urls"; -import { getScoreAsFraction, getScoreText } from "../ScoreBadge" -import Sort, { SortMode, compareScratchScores, produceSortFunction } from "../Sort" -import UserLink from "../user/UserLink" +import { getScoreAsFraction, getScoreText } from "../ScoreBadge"; +import Sort, { + SortMode, + compareScratchScores, + produceSortFunction, +} from "../Sort"; +import UserLink from "../user/UserLink"; function useFamily(scratch: TerseScratch) { - const { data: family } = useSWR(`${scratchUrl(scratch)}/family`, get, { - suspense: true, - }) + const { data: family } = useSWR( + `${scratchUrl(scratch)}/family`, + get, + { + suspense: true, + }, + ); - const [sortMode, setSortMode] = useState(SortMode.NEWEST_FIRST) - const sorted = useMemo(() => [...family].sort(produceSortFunction(sortMode)), [family, sortMode]) + const [sortMode, setSortMode] = useState(SortMode.NEWEST_FIRST); + const sorted = useMemo( + () => [...family].sort(produceSortFunction(sortMode)), + [family, sortMode], + ); - return { sorted, sortMode, setSortMode } + return { sorted, sortMode, setSortMode }; } function FamilyMember({ @@ -29,61 +40,78 @@ function FamilyMember({ isCurrent, isBetter, }: { - scratch: TerseScratch - isCurrent: boolean - isBetter: boolean + scratch: TerseScratch; + isCurrent: boolean; + isBetter: boolean; }) { - return
    - - / - {isCurrent ? - This scratch - : - {scratch.name} - } -
    -
    - {getScoreText(scratch.score, scratch.max_score, scratch.match_override)} + return ( +
    + + / + {isCurrent ? ( + This scratch + ) : ( + + {scratch.name} + + )} +
    +
    + {getScoreText( + scratch.score, + scratch.max_score, + scratch.match_override, + )} +
    -
    + ); } -export default function SortableFamilyList({ scratch }: { scratch: TerseScratch }) { - const family = useFamily(scratch) +export default function SortableFamilyList({ + scratch, +}: { scratch: TerseScratch }) { + const family = useFamily(scratch); if (family.sorted.length <= 1) { - return
    -
    -
    - No parents or forks + return ( +
    +
    +
    No parents or forks
    +

    + This scratch has no family members. It's the only + attempt at this function. +

    -

    - This scratch has no family members. - It's the only attempt at this function. -

    -
    + ); } - return
    -
    -
    - {family.sorted.length} family members + return ( +
    +
    +
    {family.sorted.length} family members
    +
    +
    -
    - +
      + {family.sorted.map((member) => ( +
    1. + +
    2. + ))} +
    -
      - {family.sorted.map(member =>
    1. - -
    2. )} -
    -
    + ); } diff --git a/frontend/src/components/Scratch/hooks/useFuzzySaveCallback.ts b/frontend/src/components/Scratch/hooks/useFuzzySaveCallback.ts index 3c7a9122f..26ba1de22 100644 --- a/frontend/src/components/Scratch/hooks/useFuzzySaveCallback.ts +++ b/frontend/src/components/Scratch/hooks/useFuzzySaveCallback.ts @@ -1,6 +1,6 @@ -import { useCallback } from "react" +import { useCallback } from "react"; -import * as api from "@/lib/api" +import * as api from "@/lib/api"; export enum FuzzySaveAction { SAVE = 0, @@ -12,28 +12,28 @@ export default function useFuzzySaveCallback( scratch: api.Scratch, setScratch: (partial: Partial) => void, ): [FuzzySaveAction, () => Promise] { - const saveScratch = api.useSaveScratch(scratch) - const forkScratch = api.useForkScratchAndGo(scratch) - const userIsYou = api.useUserIsYou() + const saveScratch = api.useSaveScratch(scratch); + const forkScratch = api.useForkScratchAndGo(scratch); + const userIsYou = api.useUserIsYou(); - let action = FuzzySaveAction.NONE + let action = FuzzySaveAction.NONE; if (userIsYou(scratch.owner)) { - action = FuzzySaveAction.SAVE + action = FuzzySaveAction.SAVE; } else { - action = FuzzySaveAction.FORK + action = FuzzySaveAction.FORK; } return [ action, useCallback(async () => { switch (action) { - case FuzzySaveAction.SAVE: - setScratch(await saveScratch()) - break - case FuzzySaveAction.FORK: - await forkScratch() - break + case FuzzySaveAction.SAVE: + setScratch(await saveScratch()); + break; + case FuzzySaveAction.FORK: + await forkScratch(); + break; } }, [action, forkScratch, saveScratch, setScratch]), - ] + ]; } diff --git a/frontend/src/components/Scratch/hooks/useLanguageServer.ts b/frontend/src/components/Scratch/hooks/useLanguageServer.ts index 02a67d779..76c050215 100644 --- a/frontend/src/components/Scratch/hooks/useLanguageServer.ts +++ b/frontend/src/components/Scratch/hooks/useLanguageServer.ts @@ -1,75 +1,99 @@ -import { type MutableRefObject, useEffect, useState } from "react" - -import type { ClangdStdioTransport, CompileCommands } from "@clangd-wasm/clangd-wasm" -import { StateEffect } from "@codemirror/state" -import type { EditorView } from "codemirror" - -import type * as api from "@/lib/api" -import { LanguageServerClient, languageServerWithTransport } from "@/lib/codemirror/languageServer" - -export default function useLanguageServer(enabled: boolean, scratch: api.Scratch, sourceEditor: MutableRefObject, contextEditor: MutableRefObject) { - const [initialScratchState, setInitialScratchState] = useState(undefined) - const [defaultClangFormat, setDefaultClangFormat] = useState(undefined) - - const [ClangdStdioTransportModule, setClangdStdioTransportModule] = useState(undefined) - - const [saveSource, setSaveSource] = useState<(source: string) => Promise>(undefined) - const [saveContext, setSaveContext] = useState<(context: string) => Promise>(undefined) +import { type MutableRefObject, useEffect, useState } from "react"; + +import type { + ClangdStdioTransport, + CompileCommands, +} from "@clangd-wasm/clangd-wasm"; +import { StateEffect } from "@codemirror/state"; +import type { EditorView } from "codemirror"; + +import type * as api from "@/lib/api"; +import { + LanguageServerClient, + languageServerWithTransport, +} from "@/lib/codemirror/languageServer"; + +export default function useLanguageServer( + enabled: boolean, + scratch: api.Scratch, + sourceEditor: MutableRefObject, + contextEditor: MutableRefObject, +) { + const [initialScratchState, setInitialScratchState] = + useState(undefined); + const [defaultClangFormat, setDefaultClangFormat] = + useState(undefined); + + const [ClangdStdioTransportModule, setClangdStdioTransportModule] = + useState(undefined); + + const [saveSource, setSaveSource] = + useState<(source: string) => Promise>(undefined); + const [saveContext, setSaveContext] = + useState<(context: string) => Promise>(undefined); useEffect(() => { const loadClangdModule = async () => { - if (!enabled) return - if (!(scratch.language === "C" || scratch.language === "C++")) return + if (!enabled) return; + if (!(scratch.language === "C" || scratch.language === "C++")) + return; - const { ClangdStdioTransport } = await import("@clangd-wasm/clangd-wasm") - setClangdStdioTransportModule(() => ClangdStdioTransport) - } + const { ClangdStdioTransport } = await import( + "@clangd-wasm/clangd-wasm" + ); + setClangdStdioTransportModule(() => ClangdStdioTransport); + }; - loadClangdModule() - }, [scratch.language, enabled]) + loadClangdModule(); + }, [scratch.language, enabled]); useEffect(() => { if (!initialScratchState) { - setInitialScratchState(scratch) + setInitialScratchState(scratch); } - }, [scratch, initialScratchState]) + }, [scratch, initialScratchState]); useEffect(() => { fetch(new URL("./default-clang-format.yaml", import.meta.url)) - .then(res => res.text()) - .then(setDefaultClangFormat) - }, []) + .then((res) => res.text()) + .then(setDefaultClangFormat); + }, []); // We break this out into a seperate effect from the module loading // because if we had _lsClient defined inside an async function, we wouldn't be // able to reference it inside of the destructor. useEffect(() => { - if (!ClangdStdioTransportModule) return - if (!initialScratchState) return - if (!defaultClangFormat) return + if (!ClangdStdioTransportModule) return; + if (!initialScratchState) return; + if (!defaultClangFormat) return; const languageId = { - "C": "c", + C: "c", "C++": "cpp", - }[initialScratchState.language] + }[initialScratchState.language]; - const sourceFilename = `source.${languageId}` - const contextFilename = `context.${languageId}` + const sourceFilename = `source.${languageId}`; + const contextFilename = `context.${languageId}`; const compileCommands: CompileCommands = [ { directory: "/", file: sourceFilename, - arguments: ["clang", sourceFilename, "-include", contextFilename], + arguments: [ + "clang", + sourceFilename, + "-include", + contextFilename, + ], }, - ] + ]; const initialFileState: Record = { ".clang-format": defaultClangFormat, - } + }; - initialFileState[sourceFilename] = initialScratchState.source_code - initialFileState[contextFilename] = initialScratchState.context + initialFileState[sourceFilename] = initialScratchState.source_code; + initialFileState[contextFilename] = initialScratchState.context; const _lsClient = new LanguageServerClient({ transport: new ClangdStdioTransportModule({ @@ -82,7 +106,7 @@ export default function useLanguageServer(enabled: boolean, scratch: api.Scratch workspaceFolders: null, documentUri: null, languageId, - }) + }); const [sourceLsExtension, _saveSource] = languageServerWithTransport({ client: _lsClient, @@ -91,7 +115,7 @@ export default function useLanguageServer(enabled: boolean, scratch: api.Scratch workspaceFolders: null, documentUri: `file:///${sourceFilename}`, languageId, - }) + }); const [contextLsExtension, _saveContext] = languageServerWithTransport({ client: _lsClient, @@ -100,32 +124,39 @@ export default function useLanguageServer(enabled: boolean, scratch: api.Scratch workspaceFolders: null, documentUri: `file:///${contextFilename}`, languageId, - }) + }); // TODO: return the codemirror extensions instead of hotpatching them in? // Given the async nature of the extension being ready, it'd require updating the Codemirror // component to support inserting extensions when the extension prop changes - sourceEditor.current?.dispatch({ effects: StateEffect.appendConfig.of(sourceLsExtension) }) - contextEditor.current?.dispatch({ effects: StateEffect.appendConfig.of(contextLsExtension) }) + sourceEditor.current?.dispatch({ + effects: StateEffect.appendConfig.of(sourceLsExtension), + }); + contextEditor.current?.dispatch({ + effects: StateEffect.appendConfig.of(contextLsExtension), + }); - setSaveSource(() => _saveSource) - setSaveContext(() => _saveContext) + setSaveSource(() => _saveSource); + setSaveContext(() => _saveContext); return () => { - _lsClient.exit() - } - - }, [ClangdStdioTransportModule, initialScratchState, defaultClangFormat, sourceEditor, contextEditor]) + _lsClient.exit(); + }; + }, [ + ClangdStdioTransportModule, + initialScratchState, + defaultClangFormat, + sourceEditor, + contextEditor, + ]); const saveSourceRet = () => { - if (saveSource) - saveSource(scratch.source_code) - } + if (saveSource) saveSource(scratch.source_code); + }; const saveContextRet = () => { - if (saveContext) - saveContext(scratch.context) - } + if (saveContext) saveContext(scratch.context); + }; - return [saveSourceRet, saveContextRet] + return [saveSourceRet, saveContextRet]; } diff --git a/frontend/src/components/Scratch/hooks/useWarnBeforeScratchUnload.ts b/frontend/src/components/Scratch/hooks/useWarnBeforeScratchUnload.ts index 37ba8995f..243596403 100644 --- a/frontend/src/components/Scratch/hooks/useWarnBeforeScratchUnload.ts +++ b/frontend/src/components/Scratch/hooks/useWarnBeforeScratchUnload.ts @@ -1,14 +1,14 @@ -import * as api from "@/lib/api" -import { useWarnBeforeUnload } from "@/lib/hooks" +import * as api from "@/lib/api"; +import { useWarnBeforeUnload } from "@/lib/hooks"; export default function useWarnBeforeScratchUnload(scratch: api.Scratch) { - const userIsYou = api.useUserIsYou() - const isSaved = api.useIsScratchSaved(scratch) + const userIsYou = api.useUserIsYou(); + const isSaved = api.useIsScratchSaved(scratch); useWarnBeforeUnload( !isSaved, userIsYou(scratch.owner) ? "You have not saved your changes to this scratch. Discard changes?" : "You have edited this scratch but not saved it in a fork. Discard changes?", - ) + ); } diff --git a/frontend/src/components/Scratch/index.ts b/frontend/src/components/Scratch/index.ts index 494561cff..2ffa942b8 100644 --- a/frontend/src/components/Scratch/index.ts +++ b/frontend/src/components/Scratch/index.ts @@ -1,4 +1,4 @@ -import Scratch, { type Props as ScratchProps } from "./Scratch" +import Scratch, { type Props as ScratchProps } from "./Scratch"; -export type Props = ScratchProps -export default Scratch +export type Props = ScratchProps; +export default Scratch; diff --git a/frontend/src/components/Scratch/panels/AboutPanel.tsx b/frontend/src/components/Scratch/panels/AboutPanel.tsx index 7d78d3657..dd62a695f 100644 --- a/frontend/src/components/Scratch/panels/AboutPanel.tsx +++ b/frontend/src/components/Scratch/panels/AboutPanel.tsx @@ -1,80 +1,95 @@ -import Link from "next/link" +import Link from "next/link"; -import useSWR from "swr" +import useSWR from "swr"; -import LoadingSpinner from "@/components/loading.svg" -import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon" -import PlatformName from "@/components/PlatformSelect/PlatformName" -import { getScoreText } from "@/components/ScoreBadge" -import TimeAgo from "@/components/TimeAgo" -import UserLink from "@/components/user/UserLink" -import { type Scratch, type Preset, get, usePreset } from "@/lib/api" -import { presetUrl, scratchUrl, scratchParentUrl } from "@/lib/api/urls" +import LoadingSpinner from "@/components/loading.svg"; +import { PlatformIcon } from "@/components/PlatformSelect/PlatformIcon"; +import PlatformName from "@/components/PlatformSelect/PlatformName"; +import { getScoreText } from "@/components/ScoreBadge"; +import TimeAgo from "@/components/TimeAgo"; +import UserLink from "@/components/user/UserLink"; +import { type Scratch, type Preset, get, usePreset } from "@/lib/api"; +import { presetUrl, scratchUrl, scratchParentUrl } from "@/lib/api/urls"; -import styles from "./AboutPanel.module.scss" +import styles from "./AboutPanel.module.scss"; function ScratchLink({ url }: { url: string }) { - const { data: scratch, error } = useSWR(url, get) + const { data: scratch, error } = useSWR(url, get); if (error) { - throw error + throw error; } if (!scratch) { - return - - + return ( + + + + ); } return ( - {scratch.name || "Untitled scratch"} - - {scratch.owner && <> - by - - } + {scratch.owner && ( + <> + by + + + )} - ) + ); } export type Props = { - scratch: Scratch - setScratch?: (scratch: Partial) => void -} + scratch: Scratch; + setScratch?: (scratch: Partial) => void; +}; export default function AboutPanel({ scratch, setScratch }: Props) { - const preset: Preset = usePreset(scratch.preset) + const preset: Preset = usePreset(scratch.preset); return (

    Score

    - {getScoreText(scratch.score, scratch.max_score, scratch.match_override)} + + {getScoreText( + scratch.score, + scratch.max_score, + scratch.match_override, + )} +
    - {
    -

    Owner

    - {scratch.owner && } -
    } - {scratch.parent &&
    -

    Fork of

    - -
    } + { +
    +

    Owner

    + {scratch.owner && } +
    + } + {scratch.parent && ( +
    +

    Fork of

    + +
    + )}

    Platform

    - +
    - {preset &&
    -

    Preset

    - - {preset.name} - -
    } + {preset && ( +
    +

    Preset

    + {preset.name} +
    + )}

    Created

    @@ -87,17 +102,23 @@ export default function AboutPanel({ scratch, setScratch }: Props) {
    - {setScratch || scratch.description ?
    -

    Description

    -