From 0e8b01841ac43fab1addde8fca0b5cc32d69a3fe Mon Sep 17 00:00:00 2001 From: Alex Bates Date: Thu, 14 Oct 2021 12:52:28 +0100 Subject: [PATCH] Frontend improvements + Docker updates (#167, #169) Co-authored-by: Mark Street <22226349+mkst@users.noreply.github.com> --- .github/workflows/pr.yml | 1 - .gitignore | 1 + .reviewdog.yml | 3 + .vscode/extensions.json | 7 + .vscode/settings.json | 15 + backend/Dockerfile | 7 + backend/compilers/.gitignore | 6 +- backend/compilers/download.sh | 7 +- backend/coreapp/compiler_wrapper.py | 26 +- backend/coreapp/sandbox.py | 1 + backend/decompme/settings.py | 3 +- docker-compose.yaml | 5 +- frontend/.dockerignore | 3 + frontend/.eslintrc.json | 50 +- frontend/.stylelintrc.json | 12 + frontend/next.config.js | 16 +- frontend/package.json | 8 +- .../ArchSelect/ArchSelect.module.scss | 68 ++ .../src/components/ArchSelect/ArchSelect.tsx | 42 ++ frontend/src/components/ArchSelect/index.ts | 4 + frontend/src/components/ArchSelect/n64.svg | 19 + frontend/src/components/ArchSelect/ps2.svg | 44 ++ .../src/components/AsyncButton.module.scss | 8 +- frontend/src/components/AsyncButton.tsx | 8 +- frontend/src/components/Button.module.scss | 9 +- frontend/src/components/Button.tsx | 10 +- frontend/src/components/Diff/Diff.module.css | 60 ++ .../src/components/{diff => Diff}/Diff.tsx | 3 +- frontend/src/components/Diff/index.ts | 4 + frontend/src/components/Editor/Editor.tsx | 4 +- .../Editor/MonacoEditor.module.scss | 10 +- .../src/components/Editor/MonacoEditor.tsx | 80 ++- frontend/src/components/Editor/language/c.ts | 62 +- .../src/components/Editor/language/mips.ts | 52 +- frontend/src/components/Editor/monacoTheme.ts | 6 +- frontend/src/components/Footer.module.scss | 6 +- frontend/src/components/Footer.tsx | 6 + .../src/components/Nav/LoginState.module.scss | 4 - frontend/src/components/Nav/LoginState.tsx | 6 +- frontend/src/components/Nav/Nav.module.scss | 47 +- frontend/src/components/Nav/Nav.tsx | 8 +- .../src/components/ScoreBadge.module.scss | 36 ++ frontend/src/components/ScoreBadge.tsx | 24 + .../Scratch/AboutScratch.module.scss | 100 +++ .../src/components/Scratch/AboutScratch.tsx | 54 ++ .../components/Scratch/Scratch.module.scss | 95 +++ .../{scratch => Scratch}/Scratch.tsx | 153 ++--- frontend/src/components/Scratch/index.ts | 4 + frontend/src/components/Select.module.scss | 7 +- frontend/src/components/Select.tsx | 6 +- frontend/src/components/Select2.tsx | 8 +- frontend/src/components/Tabs.module.scss | 20 +- frontend/src/components/Tabs.tsx | 63 +- .../compiler/CompilerButton.module.css | 2 +- .../components/compiler/CompilerButton.tsx | 8 +- .../compiler/CompilerOpts.module.css | 45 +- .../src/components/compiler/CompilerOpts.tsx | 47 +- .../src/components/compiler/PresetSelect.tsx | 32 +- .../components/compiler/compilers/index.ts | 6 +- frontend/src/components/diff/Diff.module.css | 45 -- .../src/components/scratch/Scratch.module.css | 102 --- .../src/components/user/UserLink.module.css | 14 +- frontend/src/components/user/UserLink.tsx | 18 +- frontend/src/lib/api.ts | 203 +++--- frontend/src/lib/hooks.ts | 8 +- frontend/src/pages/_app.scss | 28 +- frontend/src/pages/credits.module.scss | 57 ++ frontend/src/pages/credits.tsx | 140 +++++ frontend/src/pages/login.module.css | 2 +- frontend/src/pages/scratch.module.scss | 75 --- frontend/src/pages/scratch.tsx | 185 ------ frontend/src/pages/scratch/[slug].tsx | 4 +- frontend/src/pages/scratch/new.module.scss | 129 ++++ frontend/src/pages/scratch/new.tsx | 272 ++++++++ frontend/src/pages/theme.scss | 5 +- frontend/src/pages/u/[username].module.css | 2 +- frontend/src/pages/u/[username].tsx | 2 +- frontend/yarn.lock | 594 +++++++++++++++++- 78 files changed, 2435 insertions(+), 901 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 frontend/.dockerignore create mode 100644 frontend/.stylelintrc.json create mode 100644 frontend/src/components/ArchSelect/ArchSelect.module.scss create mode 100644 frontend/src/components/ArchSelect/ArchSelect.tsx create mode 100644 frontend/src/components/ArchSelect/index.ts create mode 100644 frontend/src/components/ArchSelect/n64.svg create mode 100644 frontend/src/components/ArchSelect/ps2.svg create mode 100644 frontend/src/components/Diff/Diff.module.css rename frontend/src/components/{diff => Diff}/Diff.tsx (96%) create mode 100644 frontend/src/components/Diff/index.ts create mode 100644 frontend/src/components/ScoreBadge.module.scss create mode 100644 frontend/src/components/ScoreBadge.tsx create mode 100644 frontend/src/components/Scratch/AboutScratch.module.scss create mode 100644 frontend/src/components/Scratch/AboutScratch.tsx create mode 100644 frontend/src/components/Scratch/Scratch.module.scss rename frontend/src/components/{scratch => Scratch}/Scratch.tsx (67%) create mode 100644 frontend/src/components/Scratch/index.ts delete mode 100644 frontend/src/components/diff/Diff.module.css delete mode 100644 frontend/src/components/scratch/Scratch.module.css create mode 100644 frontend/src/pages/credits.module.scss create mode 100644 frontend/src/pages/credits.tsx delete mode 100644 frontend/src/pages/scratch.module.scss delete mode 100644 frontend/src/pages/scratch.tsx create mode 100644 frontend/src/pages/scratch/new.module.scss create mode 100644 frontend/src/pages/scratch/new.tsx diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f0ad4f90..545659f0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -4,7 +4,6 @@ on: branches: - main pull_request: - pull_request_target: jobs: reviewdog: name: reviewdog diff --git a/.gitignore b/.gitignore index e6297c16..fb8e26da 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ sandbox/ *.log /frontend/.next /frontend/.cache +/frontend/cache /frontend/public/sw.* /frontend/public/workbox-* /frontend/storybook-static diff --git a/.reviewdog.yml b/.reviewdog.yml index 3d101a76..7933b36f 100644 --- a/.reviewdog.yml +++ b/.reviewdog.yml @@ -2,6 +2,9 @@ runner: eslint: cmd: cd frontend && yarn -s run eslint src --ext .js,.jsx,.ts,.tsx -f=rdjson format: rdjson + stylelint: + cmd: cd frontend && yarn -s run stylelint 'src/**/*.css' 'src/**/*.scss' + format: stylelint tsc: cmd: cd frontend && yarn -s run tsc format: tsc diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..fc043d5f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "EditorConfig.EditorConfig", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index fd79c9e5..763ca543 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,4 +14,19 @@ "editor.defaultFormatter": "dbaeumer.vscode-eslint", "editor.formatOnSave": true, }, + + // stylelint + "[css]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint", + "editor.formatOnSave": true, + }, + "[scss]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint", + "editor.formatOnSave": true, + }, + + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features", + "editor.formatOnSave": true, + }, } diff --git a/backend/Dockerfile b/backend/Dockerfile index 31503f10..88e3f4a7 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -36,6 +36,13 @@ RUN python3 -m pip install -r /backend/requirements.txt --no-cache-dir COPY --from=nsjail /bin/nsjail /bin/nsjail +# nsjail wants to mount this so ensure it exists +RUN mkdir -p /lib32 + +COPY compilers/download.sh /compilers/ + +RUN bash /compilers/download.sh + WORKDIR /backend ENTRYPOINT ["/backend/docker_entrypoint.sh"] diff --git a/backend/compilers/.gitignore b/backend/compilers/.gitignore index 802cd1f7..b0d0e99b 100644 --- a/backend/compilers/.gitignore +++ b/backend/compilers/.gitignore @@ -1,4 +1,4 @@ * -!gcc2.8.1/ -!ido5.3/ -!ido7.1/ +!gcc2.8.1 +!ido5.3 +!ido7.1 diff --git a/backend/compilers/download.sh b/backend/compilers/download.sh index c0b4cd94..e0cf0465 100755 --- a/backend/compilers/download.sh +++ b/backend/compilers/download.sh @@ -13,11 +13,14 @@ else fi # gcc2.8.1 +mkdir -p "$compiler_dir/gcc2.8.1" curl -L "https://github.com/pmret/gcc-papermario/releases/download/master/$os.tar.gz" | tar zx -C "$compiler_dir/gcc2.8.1" curl -L "https://github.com/pmret/binutils-papermario/releases/download/master/$os.tar.gz" | tar zx -C "$compiler_dir/gcc2.8.1" # ido5.3 -curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-5.3-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido5.3/" +mkdir -p "$compiler_dir/ido5.3" +curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-5.3-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido5.3" # ido7.1 -curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-7.1-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido7.1/" +mkdir -p "$compiler_dir/ido7.1" +curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-7.1-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido7.1" diff --git a/backend/coreapp/compiler_wrapper.py b/backend/coreapp/compiler_wrapper.py index 2e16e1c2..90aa7ea8 100644 --- a/backend/coreapp/compiler_wrapper.py +++ b/backend/coreapp/compiler_wrapper.py @@ -1,4 +1,5 @@ from typing import Dict, List, Optional, Set, Tuple +from collections import OrderedDict from coreapp.models import Asm, Assembly, Compilation from coreapp import util from coreapp.sandbox import Sandbox @@ -38,9 +39,10 @@ else: def load_compilers() -> Dict[str, Dict[str, str]]: ret = {} - compiler_dirs = next(os.walk(settings.COMPILER_BASE_PATH)) + compilers_base = settings.BASE_DIR / "compilers" + compiler_dirs = next(os.walk(compilers_base)) for compiler_id in compiler_dirs[1]: - config_path = Path(settings.COMPILER_BASE_PATH / compiler_id / "config.json") + config_path = Path(compilers_base / compiler_id / "config.json") if config_path.exists(): with open(config_path) as f: ret[compiler_id] = json.load(f) @@ -51,6 +53,7 @@ def load_compilers() -> Dict[str, Dict[str, str]]: @dataclass class Arch: name: str + description: str assemble_cmd: Optional[str] = None objdump_cmd: Optional[str] = None nm_cmd: Optional[str] = None @@ -59,13 +62,15 @@ class Arch: def load_arches() -> Dict[str, Arch]: return { "mips": Arch( - "MIPS (Nintendo 64)", + "Nintendo 64", + "MIPS (big-endian)", assemble_cmd='mips-linux-gnu-as -march=vr4300 -mabi=32 -o "$OUTPUT" "$INPUT"', objdump_cmd="mips-linux-gnu-objdump", nm_cmd="mips-linux-gnu-nm", ), "mipsel": Arch( - "MIPS (LE)", + "PlayStation 2", + "MIPS (little-endian)", assemble_cmd='mips-linux-gnu-as -march=mips64 -mabi=64 -o "$OUTPUT" "$INPUT"', objdump_cmd="mips-linux-gnu-objdump", nm_cmd="mips-linux-gnu-nm", @@ -120,17 +125,18 @@ class CompilerWrapper: return {k: {"arch": CompilerWrapper.arch_from_compiler(k)} for k in CompilerWrapper.available_compiler_ids()} @staticmethod - def available_arches() -> List[Tuple[str, str]]: + def available_arches() -> OrderedDict[str, Dict[str, str]]: a_set: Set[str] = set() - ret = [] + ret = OrderedDict() for id in CompilerWrapper.available_compiler_ids(): a_set.add(_compilers[id]["arch"]) - for a in a_set: - ret.append((a, _arches[a].name)) - - ret.sort(key=lambda x: x[0]) + for a in sorted(a_set): + ret[a] = { + "name": _arches[a].name, + "description": _arches[a].description, + } return ret diff --git a/backend/coreapp/sandbox.py b/backend/coreapp/sandbox.py index 01629ad2..504f9f46 100644 --- a/backend/coreapp/sandbox.py +++ b/backend/coreapp/sandbox.py @@ -55,6 +55,7 @@ class Sandbox(contextlib.AbstractContextManager["Sandbox"]): "--bindmount_ro", "/lib32", "--bindmount_ro", "/lib64", "--bindmount_ro", "/usr", + "--bindmount_ro", str(settings.COMPILER_BASE_PATH), "--env", "PATH=/usr/bin:/bin", "--disable_proc", # Needed for running inside Docker "--time_limit", "30", # seconds diff --git a/backend/decompme/settings.py b/backend/decompme/settings.py index 41a365bd..a62a82c6 100644 --- a/backend/decompme/settings.py +++ b/backend/decompme/settings.py @@ -25,6 +25,7 @@ env = environ.Env( SESSION_COOKIE_SECURE=(bool, True), GITHUB_CLIENT_ID=(str, ""), GITHUB_CLIENT_SECRET=(str, ""), + COMPILER_BASE_PATH=(str, BASE_DIR / "compilers") ) for stem in [".env.local", ".env"]: @@ -150,7 +151,7 @@ if DEBUG: else: SESSION_COOKIE_SAMESITE = "Lax" -COMPILER_BASE_PATH = BASE_DIR / "compilers" +COMPILER_BASE_PATH = Path(env("COMPILER_BASE_PATH")) LOCAL_FILE_DIR = BASE_DIR / "local_files" USE_SANDBOX_JAIL = env("USE_SANDBOX_JAIL") diff --git a/docker-compose.yaml b/docker-compose.yaml index 0e2c5924..2e33381e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,8 +21,9 @@ services: DATABASE_URL: psql://decompme:decompme@postgres:5432/decompme SECRET_KEY: "django-insecure-nm#!8%z$$hc0wwi#m_*l9l)=m*6gs4&o_^-e5b5vj*k05&yaqc1" DEBUG: "on" - ALLOWED_HOSTS: "localhost,127.0.0.1" + ALLOWED_HOSTS: "backend,localhost,127.0.0.1" USE_SANDBOX_JAIL: "on" + COMPILER_BASE_PATH: /compilers ports: - "8000:8000" security_opt: @@ -38,6 +39,8 @@ services: size: 64M frontend: build: frontend + environment: + INTERNAL_API_BASE: http://backend:8000/api ports: - "8080:8080" volumes: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..cc48fabc --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,3 @@ +.next/ +cache/ +node_modules/ diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 0e0181da..b1fee051 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -1,18 +1,55 @@ { - "extends": "next/core-web-vitals", + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint", + "css-modules" + ], + "extends": [ + "eslint:recommended", + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended" + ], "rules": { "semi": ["error", "never", { "beforeStatementContinuationChars": "always" }], "indent": ["error", 4], "quotes": ["error", "double"], "quote-props": ["error", "consistent"], - "brace-style": "error", + "brace-style": "off", + "@typescript-eslint/brace-style": ["error"], "object-curly-spacing": ["error", "always"], "array-bracket-spacing": ["error", "never"], "no-else-return": "off", "no-trailing-spaces": "error", + "no-multi-spaces": "error", + "no-multiple-empty-lines": "error", + "comma-dangle": "off", + "@typescript-eslint/comma-dangle": ["error", "always-multiline"], + "comma-spacing": "off", + "@typescript-eslint/comma-spacing": ["error"], "prefer-const": ["warn", { "destructuring": "all" }], "arrow-parens": ["error", "as-needed"], "no-confusing-arrow": ["error", { "allowParens": true }], + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "no-extra-semi": "off", + "@typescript-eslint/no-extra-semi": ["error"], + "no-empty-function": "off", + "@typescript-eslint/no-empty-function": "off", + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": ["warn"], + "@typescript-eslint/ban-ts-comment": "off", + "keyword-spacing": "off", + "@typescript-eslint/keyword-spacing": ["error"], + "@typescript-eslint/member-delimiter-style": ["error", { + "multiline": { + "delimiter": "none", + "requireLast": true + }, + "singleline": { + "delimiter": "comma", + "requireLast": false + }, + "multilineDetection": "brackets" + }], "import/newline-after-import": "error", "import/no-duplicates": "error", "import/first": "error", @@ -42,7 +79,14 @@ "pathGroupsExcludedImportTypes": ["react"], "alphabetize": { "order": "asc", "caseInsensitive": true } } - ] + ], + "import/no-unresolved": "error", + "import/export": "warn", + "import/no-named-as-default": "warn", + "import/no-named-as-default-member": "warn", + "import/no-unused-modules": "warn", + "css-modules/no-unused-class": "warn", + "css-modules/no-undef-class": "warn" }, "overrides": [ { diff --git a/frontend/.stylelintrc.json b/frontend/.stylelintrc.json new file mode 100644 index 00000000..7c261d11 --- /dev/null +++ b/frontend/.stylelintrc.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "stylelint-config-standard", + "stylelint-config-css-modules" + ], + "rules": { + "indentation": 4, + "declaration-empty-line-before": null, + "at-rule-no-unknown": null, + "no-descending-specificity": null + } +} diff --git a/frontend/next.config.js b/frontend/next.config.js index 6fe38d23..592d3b9f 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -12,7 +12,14 @@ for (const envFile of [".env.local", ".env"]) { process.env.NEXT_PUBLIC_API_BASE = process.env.API_BASE process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID -process.env.NEXT_PUBLIC_COMMIT_HASH = execSync("git rev-parse HEAD").toString().trim() +let git_hash +try { + git_hash = execSync("git rev-parse HEAD").toString().trim() +} catch (error) { + console.log("Unable to get git hash, assume running inside Docker") + git_hash = "abc123" +} +process.env.NEXT_PUBLIC_COMMIT_HASH = git_hash const withPWA = require('next-pwa') const runtimeCaching = require('next-pwa/cache') @@ -22,9 +29,14 @@ module.exports = removeImports(withPWA({ return [ { source: "/", - destination: "/scratch", + destination: "/scratch/new", permanent: false, }, + { + source: "/scratch", + destination: "/scratch/new", + permanent: true, + }, ] }, async rewrites() { diff --git a/frontend/package.json b/frontend/package.json index c5236d66..44faa57d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "dev": "next dev --port 8080", "build": "next build", "start": "next start --port 8080", - "lint": "next lint", + "lint": "next lint && yarn stylelint 'src/**/*.css' 'src/**/*.scss'", "postinstall": "next telemetry disable > /dev/null", "storybook": "start-storybook -p 6006 --ci", "build-storybook": "build-storybook" @@ -43,6 +43,8 @@ "@storybook/preset-scss": "^1.0.3", "@storybook/react": "^6.3.10", "@svgr/webpack": "^5.5.0", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", "autoprefixer": "^10.3.1", "babel-loader": "^8.2.2", "css-loader": "^6.4.0", @@ -51,6 +53,7 @@ "eslint": "7", "eslint-config-next": "11.1.2", "eslint-formatter-rdjson": "^1.0.5", + "eslint-plugin-css-modules": "^2.11.0", "eslint-plugin-react": "^7.26.1", "eslint-plugin-react-hooks": "^4.2.0", "monaco-editor-webpack-plugin": "^5.0.0", @@ -61,6 +64,9 @@ "sass-loader": "^12.1.0", "storybook-dark-mode": "^1.0.8", "style-loader": "^3.3.0", + "stylelint": "^13.13.1", + "stylelint-config-css-modules": "^2.2.0", + "stylelint-config-standard": "^22.0.0", "typescript": "^4.4.2", "webpack": "5" } diff --git a/frontend/src/components/ArchSelect/ArchSelect.module.scss b/frontend/src/components/ArchSelect/ArchSelect.module.scss new file mode 100644 index 00000000..cd0a7038 --- /dev/null +++ b/frontend/src/components/ArchSelect/ArchSelect.module.scss @@ -0,0 +1,68 @@ +.container { + list-style: none; + + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; +} + +.arch { + flex: 1; + + display: flex; + align-items: center; + gap: 16px; + + padding: 16px; + + border: 1px solid transparent; + border-radius: 6px; + background: var(--g300); + color: var(--g1200); + + cursor: pointer; + user-select: none; + + white-space: nowrap; + + transition: border-color 0.1s, background 0.15s, color 0.2s; + + svg { + width: 40px; + height: 40px; + + will-change: filter; + filter: grayscale(100%); + transition: filter 0.2s; + } + + &:hover, + &.selected { + border-color: var(--g400); + color: var(--g2000); + + svg { + filter: grayscale(0%); + } + } + + &.selected { + background: var(--g400); + } +} + +.labelContainer { + display: flex; + flex-direction: column; + gap: 2px; +} + +.consoleName { + font-weight: 500; +} + +.archName { + font-size: 0.8em; + opacity: 0.6; +} diff --git a/frontend/src/components/ArchSelect/ArchSelect.tsx b/frontend/src/components/ArchSelect/ArchSelect.tsx new file mode 100644 index 00000000..7940e8bf --- /dev/null +++ b/frontend/src/components/ArchSelect/ArchSelect.tsx @@ -0,0 +1,42 @@ +import classNames from "classnames" + +import styles from "./ArchSelect.module.scss" +import LogoN64 from "./n64.svg" +import LogoPS2 from "./ps2.svg" + +const ICONS = { + "mips": , + "mipsel": , +} + +export type Props = { + arches: { + [key: string]: { + name: string + description: string + } + } + value: string + className?: string + onChange: (value: string) => void +} + +export default function ArchSelect({ arches, value, onChange, className }: Props) { + if (!value) + onChange("mips") + + + return
    + {Object.entries(arches).map(([key, arch]) =>
  • onChange(key)} + > + {ICONS[key]} +
    +
    {arch.name}
    +
    {arch.description}
    +
    +
  • )} +
+} diff --git a/frontend/src/components/ArchSelect/index.ts b/frontend/src/components/ArchSelect/index.ts new file mode 100644 index 00000000..12172535 --- /dev/null +++ b/frontend/src/components/ArchSelect/index.ts @@ -0,0 +1,4 @@ +import ArchSelect, { Props as ArchSelectProps } from "./ArchSelect" + +export type Props = ArchSelectProps +export default ArchSelect diff --git a/frontend/src/components/ArchSelect/n64.svg b/frontend/src/components/ArchSelect/n64.svg new file mode 100644 index 00000000..6be4e13d --- /dev/null +++ b/frontend/src/components/ArchSelect/n64.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/ArchSelect/ps2.svg b/frontend/src/components/ArchSelect/ps2.svg new file mode 100644 index 00000000..7cc0aa41 --- /dev/null +++ b/frontend/src/components/ArchSelect/ps2.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + PlayStation 2 logo + + + + diff --git a/frontend/src/components/AsyncButton.module.scss b/frontend/src/components/AsyncButton.module.scss index e679ec18..d338ce3b 100644 --- a/frontend/src/components/AsyncButton.module.scss +++ b/frontend/src/components/AsyncButton.module.scss @@ -1,6 +1,6 @@ .errorPopup { font-size: 0.8rem; - background: #bb4444; + background: #b44; color: var(--a800); padding: 1rem; border-radius: 0.4rem; @@ -20,11 +20,11 @@ .label { opacity: 1; transform: initial; - transition: opacity .1s ease, transform .1s ease; + transition: opacity 0.1s ease, transform 0.1s ease; .isLoading & { opacity: 0; - transform:scale(0.9); + transform: scale(0.9); } } @@ -41,7 +41,7 @@ opacity: 0; transform: scale(0.9); - transition: opacity .1s ease, transform .2s ease; + transition: opacity 0.1s ease, transform 0.2s ease; .isLoading & { opacity: initial; diff --git a/frontend/src/components/AsyncButton.tsx b/frontend/src/components/AsyncButton.tsx index 2bd42ed3..1d83ea5d 100644 --- a/frontend/src/components/AsyncButton.tsx +++ b/frontend/src/components/AsyncButton.tsx @@ -9,10 +9,10 @@ import Button, { 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) { diff --git a/frontend/src/components/Button.module.scss b/frontend/src/components/Button.module.scss index 640ff803..8754c3fe 100644 --- a/frontend/src/components/Button.module.scss +++ b/frontend/src/components/Button.module.scss @@ -1,21 +1,22 @@ .btn { border-radius: 4px; - padding: .6em 1em; + padding: 0.6em 1em; - font-size: .8rem; + font-size: 0.8rem; + font-weight: 500; user-select: none; appearance: none; display: inline-flex; - gap: .5em; + gap: 0.5em; align-items: center; background: transparent; color: var(--a800); border: 1px solid var(--g500); - transition: color .2s ease, background .15s ease, border-color .15s ease; + transition: color 0.2s ease, background 0.15s ease, border-color 0.15s ease; &:disabled { opacity: 0.7; diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index d939baa1..80e54143 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -28,11 +28,11 @@ const Button = forwardRef(function Button({ }) export type Props = { - children: React.ReactNode, - onClick: (event: React.MouseEvent) => void, - className?: string, - disabled?: boolean, - primary?: boolean, + children: React.ReactNode + onClick: (event: React.MouseEvent) => void + className?: string + disabled?: boolean + primary?: boolean } export default Button diff --git a/frontend/src/components/Diff/Diff.module.css b/frontend/src/components/Diff/Diff.module.css new file mode 100644 index 00000000..b4b5d4d2 --- /dev/null +++ b/frontend/src/components/Diff/Diff.module.css @@ -0,0 +1,60 @@ +.container { + padding: 8px; + overflow: auto; + width: 100%; + + scrollbar-color: #fff3 transparent; + scrollbar-width: thin; +} + +.diff { + border-spacing: 1em 0; +} + +.diff, +.log { + display: block; + user-select: text; + border: none; + font-family: monospace; + font-size: 12px; + white-space: pre; + width: 100%; +} + +.lineNumber { + color: grey; + font-size: 0.7em; + font-style: italic; +} + +.immediate { color: #6d6dff; } +.stack { color: #e3fc45; } +.register { color: #e3fc45; } + +.delay_slot { + font-weight: bold; + color: #969896; +} + +.diff_change { color: #6d6dff; } +.diff_add { color: #45bd00; } +.diff_remove { color: #c82829; } +.source_filename { font-weight: bold; } + +.source_function { + font-weight: bold; + text-decoration: underline; +} + +.source_other { font-style: italic; } +.source_line_num { font-style: italic; } +.rotation0 { color: magenta; } +.rotation1 { color: cyan; } +.rotation2 { color: rgb(0, 212, 0); } +.rotation3 { color: red; } +.rotation4 { color: lightyellow; } +.rotation5 { color: lightpink; } +.rotation6 { color: lightcyan; } +.rotation7 { color: lightgreen; } +.rotation8 { color: grey; } diff --git a/frontend/src/components/diff/Diff.tsx b/frontend/src/components/Diff/Diff.tsx similarity index 96% rename from frontend/src/components/diff/Diff.tsx rename to frontend/src/components/Diff/Diff.tsx index 85f8d453..145eebe5 100644 --- a/frontend/src/components/diff/Diff.tsx +++ b/frontend/src/components/Diff/Diff.tsx @@ -1,3 +1,4 @@ +/* eslint css-modules/no-unused-class: off */ import * as api from "../../lib/api" @@ -18,7 +19,7 @@ function FormatDiffText({ texts }: { texts: api.DiffText[] }) { } export type Props = { - compilation: api.Compilation, + compilation: api.Compilation } export default function Diff({ compilation }: Props) { diff --git a/frontend/src/components/Diff/index.ts b/frontend/src/components/Diff/index.ts new file mode 100644 index 00000000..c65c3a87 --- /dev/null +++ b/frontend/src/components/Diff/index.ts @@ -0,0 +1,4 @@ +import Diff, { Props as DiffProps } from "./Diff" + +export type Props = DiffProps +export default Diff diff --git a/frontend/src/components/Editor/Editor.tsx b/frontend/src/components/Editor/Editor.tsx index 2935d6bb..fa4d8252 100644 --- a/frontend/src/components/Editor/Editor.tsx +++ b/frontend/src/components/Editor/Editor.tsx @@ -14,8 +14,8 @@ import getTheme from "./monacoTheme" const isMobile = mobile() interface Props extends MonacoEditorProps { - bubbleSuspense?: boolean, - useLoadingSpinner?: boolean, + bubbleSuspense?: boolean + useLoadingSpinner?: boolean } const MonacoEditor = isMobile ? null : dynamic(() => import("./MonacoEditor"), { diff --git a/frontend/src/components/Editor/MonacoEditor.module.scss b/frontend/src/components/Editor/MonacoEditor.module.scss index 936eff92..b25721ae 100644 --- a/frontend/src/components/Editor/MonacoEditor.module.scss +++ b/frontend/src/components/Editor/MonacoEditor.module.scss @@ -1,9 +1,9 @@ .container { flex-grow: 1; - user-select: initial; /* fix border-radius overflow */ overflow: hidden; + & :global(.monaco-editor), & :global(.monaco-editor-background), & :global(.margin), @@ -15,6 +15,14 @@ & :global(.monaco-hover) { display: none; } + + & :global(.mac .monaco-mouse-cursor-text) { + cursor: text !important; + } + + & :global(.view-lines) { + user-select: initial; + } } .readonly { diff --git a/frontend/src/components/Editor/MonacoEditor.tsx b/frontend/src/components/Editor/MonacoEditor.tsx index 78fe00cc..d67061d8 100644 --- a/frontend/src/components/Editor/MonacoEditor.tsx +++ b/frontend/src/components/Editor/MonacoEditor.tsx @@ -1,8 +1,7 @@ -import { useEffect, useState, useRef } from "react" +import { useEffect, useState, useRef, MutableRefObject } from "react" import classNames from "classnames" -import * as monaco from "monaco-editor" -import { editor } from "monaco-editor" +import { editor, languages } from "monaco-editor" import * as c from "./language/c" import * as mips from "./language/mips" @@ -15,13 +14,13 @@ if (typeof window === "undefined") { throw new Error("Editor component does not work with SSR, use next/dynamic with { ssr: false }") } -monaco.languages.register({ id: "decompme_c" }) -monaco.languages.setLanguageConfiguration("decompme_c", c.conf) -monaco.languages.setMonarchTokensProvider("decompme_c", c.language) +languages.register({ id: "decompme_c" }) +languages.setLanguageConfiguration("decompme_c", c.conf) +languages.setMonarchTokensProvider("decompme_c", c.language) -monaco.languages.register({ id: "decompme_mips" }) -monaco.languages.setLanguageConfiguration("decompme_mips", mips.conf) -monaco.languages.setMonarchTokensProvider("decompme_mips", mips.language) +languages.register({ id: "decompme_mips" }) +languages.setLanguageConfiguration("decompme_mips", mips.conf) +languages.setMonarchTokensProvider("decompme_mips", mips.language) function convertLanguage(language: string) { if (language === "c") @@ -32,30 +31,34 @@ function convertLanguage(language: string) { return "plaintext" } +export type EditorInstance = editor.IStandaloneCodeEditor; + export type Props = { - className?: string, + className?: string // This is a controlled component - value: string, - onChange?: (value: string) => void, + value: string + onChange?: (value: string) => void + + instanceRef?: MutableRefObject // Options - language: "c" | "mips", - lineNumbers?: boolean, - showMargin?: boolean, - padding?: number, // css + language: "c" | "mips" + lineNumbers?: boolean + showMargin?: boolean + padding?: number // css } -export default function Editor({ value, onChange, className, showMargin, padding, language, lineNumbers }: Props) { +export default function Editor({ value, onChange, className, showMargin, padding, language, lineNumbers, instanceRef }: Props) { const isReadOnly = typeof onChange === "undefined" const containerRef = useRef(null) - const [editor, setEditor] = useState(null) + const [editorInstance, setEditorInstance] = useState(null) // Effect to set up the editor. This is run once when the component is mounted. useEffect(() => { - monaco.editor.defineTheme("custom", monacoTheme()) + editor.defineTheme("custom", monacoTheme()) - const editor = monaco.editor.create(containerRef.current, { + const editorInstance = editor.create(containerRef.current, { language: convertLanguage(language), value, theme: "custom", @@ -83,12 +86,14 @@ export default function Editor({ value, onChange, className, showMargin, padding glyphMargin: !!showMargin, folding: !!showMargin, lineDecorationsWidth: padding ?? (showMargin ? 10 : 0), - lineNumbersMinChars: showMargin ? 5 : 0, + lineNumbersMinChars: showMargin ? 2 : 0, automaticLayout: true, }) - setEditor(editor) + setEditorInstance(editorInstance) + if (instanceRef) + instanceRef.current = editorInstance - const model = editor.getModel() + const model = editorInstance.getModel() if (model) { model.onDidChangeContent(() => { if (onChange) @@ -98,33 +103,42 @@ export default function Editor({ value, onChange, className, showMargin, padding console.error("monaco editor has no model") } - return () => editor.dispose() + return () => editorInstance.dispose() }, []) // eslint-disable-line react-hooks/exhaustive-deps // Update value. useEffect(() => { - const model = editor?.getModel() + const model = editorInstance?.getModel() // Only update the model value if it is different; otherwise, the // model state will be reset every time the user types! if (model && model.getValue() !== value) { - console.log("editor value reset") + console.warn("editor value reset") model.setValue(value) } - }, [editor, value]) + }, [editorInstance, value]) // Update language. useEffect(() => { - const model = editor?.getModel() + const model = editorInstance?.getModel() if (model) { - monaco.editor.setModelLanguage(model, convertLanguage(language)) + editor.setModelLanguage(model, convertLanguage(language)) } - }, [editor, language]) + }, [editorInstance, language]) useEffect(() => { - editor?.updateOptions({ lineNumbers: lineNumbers ? "on" : "off" }) - }, [editor, lineNumbers]) + editorInstance?.updateOptions({ lineNumbers: lineNumbers ? "on" : "off" }) + }, [editorInstance, lineNumbers]) + + useEffect(() => { + editorInstance?.updateOptions({ + glyphMargin: !!showMargin, + folding: !!showMargin, + lineDecorationsWidth: padding ?? (showMargin ? 10 : 0), + lineNumbersMinChars: showMargin ? 2 : 0, + }) + }, [editorInstance, padding, showMargin]) return
" } + { token: "delimiter.angle", open: "<", close: ">" }, ], keywords: [ @@ -309,9 +309,9 @@ export const language: languages.IMonarchLanguage = { cases: { "@keywords": { token: "keyword.$0" }, "@types": { token: "storage.type.$0" }, - "@default": "identifier" - } - } + "@default": "identifier", + }, + }, ], // The preprocessor checks must be before whitespace as they check /^\s*#/ which @@ -337,9 +337,9 @@ export const language: languages.IMonarchLanguage = { cases: { "@comparisonOperators": { token: "operator.comparison" }, "@operators": { token: "operator" }, - "@default": { token: "delimiter" } - } - } + "@default": { token: "delimiter" }, + }, + }, ], // numbers @@ -361,7 +361,7 @@ export const language: languages.IMonarchLanguage = { // characters [/'[^\\']'/, "string"], [/(')(@escapes)(')/, ["string", "string.escape", "string"]], - [/'/, "string.invalid"] + [/'/, "string.invalid"], ], whitespace: [ @@ -369,33 +369,33 @@ export const language: languages.IMonarchLanguage = { [/\/\*\*(?!\/)/, "comment.doc", "@doccomment"], [/\/\*/, "comment", "@comment"], [/\/\/.*\\$/, "comment", "@linecomment"], - [/\/\/.*$/, "comment"] + [/\/\/.*$/, "comment"], ], comment: [ [/[^/*]+/, "comment"], [/\*\//, "comment", "@pop"], - [/[/*]/, "comment"] + [/[/*]/, "comment"], ], //For use with continuous line comments linecomment: [ [/.*[^\\]$/, "comment", "@pop"], - [/[^]+/, "comment"] + [/[^]+/, "comment"], ], //Identical copy of comment above, except for the addition of .doc doccomment: [ [/[^/*]+/, "comment.doc"], [/\*\//, "comment.doc", "@pop"], - [/[/*]/, "comment.doc"] + [/[/*]/, "comment.doc"], ], string: [ [/[^\\"]+/, "string"], [/@escapes/, "string.escape"], [/\\./, "string.escape.invalid"], - [/"/, "string", "@pop"] + [/"/, "string", "@pop"], ], raw: [ @@ -407,13 +407,13 @@ export const language: languages.IMonarchLanguage = { "string.raw", "string.raw.end", "string.raw.end", - { token: "string.raw.end", next: "@pop" } + { token: "string.raw.end", next: "@pop" }, ], - "@default": ["string.raw", "string.raw", "string.raw", "string.raw"] - } - } + "@default": ["string.raw", "string.raw", "string.raw", "string.raw"], + }, + }, ], - [/.*/, "string.raw"] + [/.*/, "string.raw"], ], annotation: [ @@ -422,7 +422,7 @@ export const language: languages.IMonarchLanguage = { [/[a-zA-Z0-9_]+/, "annotation"], [/[,:]/, "delimiter"], [/[()]/, "@brackets"], - [/\]\s*\]/, { token: "annotation", next: "@pop" }] + [/\]\s*\]/, { token: "annotation", next: "@pop" }], ], include: [ @@ -432,8 +432,8 @@ export const language: languages.IMonarchLanguage = { "", "keyword.directive.include.begin", "string.include.identifier", - { token: "keyword.directive.include.end", next: "@pop" } - ] as languages.IMonarchLanguageAction + { token: "keyword.directive.include.end", next: "@pop" }, + ] as languages.IMonarchLanguageAction, ], [ /(\s*)(")([^"]*)(")/, @@ -441,9 +441,9 @@ export const language: languages.IMonarchLanguage = { "", "keyword.directive.include.begin", "string.include.identifier", - { token: "keyword.directive.include.end", next: "@pop" } - ] as languages.IMonarchLanguageAction - ] - ] - } + { token: "keyword.directive.include.end", next: "@pop" }, + ] as languages.IMonarchLanguageAction, + ], + ], + }, } diff --git a/frontend/src/components/Editor/language/mips.ts b/frontend/src/components/Editor/language/mips.ts index 843777b9..f8985feb 100644 --- a/frontend/src/components/Editor/language/mips.ts +++ b/frontend/src/components/Editor/language/mips.ts @@ -3,14 +3,14 @@ import type { languages } from "monaco-editor" export const conf: languages.LanguageConfiguration = { comments: { lineComment: "#", - blockComment: ["/*", "*/"] + blockComment: ["/*", "*/"], }, brackets: [ ["(", ")"], ], autoClosingPairs: [ { open: "(", close: ")" }, - { open: "\"", close: "\"", notIn: ["string"] } + { open: "\"", close: "\"", notIn: ["string"] }, ], surroundingPairs: [ { open: "(", close: ")" }, @@ -32,7 +32,7 @@ export const language: languages.IMonarchLanguage = { instructions: [ "lb", "lbu", "ld", "ldl", "ldr", "lh", "lhu", "ll", "lld", "lw", "lwl", "lwr", "lwu", "sb", "sc", "scd", "sd", "sdl", "sdr", "sh", "sw", "swl", "swr", "sync", "add", "addi", "addiu", "addu", "and", "andi", "dadd", "daddi", "daddiu", "daddu", "ddiv", "ddivu", "div", "divu", "dmult", "dmultu", "dsll", "dsll32", "dsllv", "dsra", "dsra32", "dsrav", "dsrl", "dsrl32", "dsrlv", "dsub", "dsubu", "lui", "mfhi", "mflo", "mthi", "mtlo", "mult", "multu", "nor", "or", "ori", "sll", "sllv", "slt", "slti", "sltiu", "sltu", "sra", "srav", "srl", "srlv", "sub", "subu", "xor", "xori", "beq", "beql", "bgez", "bgezal", "bgezall", "bgezl", "bgtz", "bgtzl", "blez", "blezl", "bltz", "bltzal", "bltzall", "bltzl", "bne", "bnel", "j", "jal", "jalr", "jr", "break", "syscall", "teq", "teqi", "tge", "tgei", "tgeiu", "tgeu", "tlt", "tlti", "tltiu", "tltu", "tne", "tnei", "cache", "dmfc0", "dmtc0", "eret", "mfc0", "mtc0", "tlbp", "tlbr", "tlbwi", "tlbwr", "bc1f", "bc1fl", "bc1t", "bc1tl", "cfc1", "ctc1", "dmfc1", "dmtc1", "ldc1", "lwc1", "mfc1", "mtc1", "sdc1", "swc1", - "beqz", "bnez", "negu", + "beqz", "bnez", "negu", "nop", ], registers: [ @@ -58,18 +58,18 @@ export const language: languages.IMonarchLanguage = { "jal": { token: "function" }, "@instructions": { token: "support.function.$0" }, "@keywords": { token: "keyword.$0" }, - "@default": "identifier" - } - } + "@default": "identifier", + }, + }, ], [ /\$\w+/, { cases: { "@registers": { token: "entity.name.register.$0" }, - "@default": "identifier" - } - } + "@default": "identifier", + }, + }, ], [/%(hi|lo)/, "macro"], [/\.\w+/, { token: "keyword.directive" }], @@ -96,33 +96,33 @@ export const language: languages.IMonarchLanguage = { // characters [/'[^\\']'/, "string"], [/(')(@escapes)(')/, ["string", "string.escape", "string"]], - [/'/, "string.invalid"] + [/'/, "string.invalid"], ], whitespace: [ [/[ \t\r\n]+/, ""], [/\/\*/, "comment", "@comment"], [/#.*\\$/, "comment", "@linecomment"], - [/#.*$/, "comment"] + [/#.*$/, "comment"], ], comment: [ [/[^/*]+/, "comment"], [/\*\//, "comment", "@pop"], - [/[/*]/, "comment"] + [/[/*]/, "comment"], ], //For use with continuous line comments linecomment: [ [/.*[^#]$/, "comment", "@pop"], - [/[^]+/, "comment"] + [/[^]+/, "comment"], ], string: [ [/[^\\"]+/, "string"], [/@escapes/, "string.escape"], [/\\./, "string.escape.invalid"], - [/"/, "string", "@pop"] + [/"/, "string", "@pop"], ], raw: [ @@ -134,13 +134,13 @@ export const language: languages.IMonarchLanguage = { "string.raw", "string.raw.end", "string.raw.end", - { token: "string.raw.end", next: "@pop" } + { token: "string.raw.end", next: "@pop" }, ], - "@default": ["string.raw", "string.raw", "string.raw", "string.raw"] - } - } + "@default": ["string.raw", "string.raw", "string.raw", "string.raw"], + }, + }, ], - [/.*/, "string.raw"] + [/.*/, "string.raw"], ], include: [ @@ -150,8 +150,8 @@ export const language: languages.IMonarchLanguage = { "", "keyword.directive.include.begin", "string.include.identifier", - { token: "keyword.directive.include.end", next: "@pop" } - ] as languages.IMonarchLanguageAction + { token: "keyword.directive.include.end", next: "@pop" }, + ] as languages.IMonarchLanguageAction, ], [ /(\s*)(")([^"]*)(")/, @@ -159,9 +159,9 @@ export const language: languages.IMonarchLanguage = { "", "keyword.directive.include.begin", "string.include.identifier", - { token: "keyword.directive.include.end", next: "@pop" } - ] as languages.IMonarchLanguageAction - ] - ] - } + { token: "keyword.directive.include.end", next: "@pop" }, + ] as languages.IMonarchLanguageAction, + ], + ], + }, } diff --git a/frontend/src/components/Editor/monacoTheme.ts b/frontend/src/components/Editor/monacoTheme.ts index e9d2666c..15046dde 100644 --- a/frontend/src/components/Editor/monacoTheme.ts +++ b/frontend/src/components/Editor/monacoTheme.ts @@ -23,7 +23,7 @@ export default function getTheme(): editor.IStandaloneThemeData { { "token": "entity.other.attribute-name", "foreground": "FF4A98" }, { "token": "support.function", "foreground": "45B8FF" }, { "token": "storage", "foreground": "45B8FF" }, - { "token": "macro", "foreground": "3bff6c" } + { "token": "macro", "foreground": "3bff6c" }, ], "colors": { "editor.foreground": "#8599b3", @@ -32,7 +32,7 @@ export default function getTheme(): editor.IStandaloneThemeData { "editor.lineHighlightBackground": "#ccccff07", "editorCursor.foreground": "#c9cbfc", "editorWhitespace.foreground": "#c9cbfc11", - "editorLineNumber.foreground":"#ccccff31" - } + "editorLineNumber.foreground":"#ccccff31", + }, } } diff --git a/frontend/src/components/Footer.module.scss b/frontend/src/components/Footer.module.scss index c9a3c0e8..86a914ee 100644 --- a/frontend/src/components/Footer.module.scss +++ b/frontend/src/components/Footer.module.scss @@ -19,12 +19,12 @@ .links { display: flex; - justify-content: space-around; + justify-content: space-evenly; } .link { display: inline-flex; - gap: .5em; + gap: 0.5em; align-items: center; padding: 1em; @@ -37,7 +37,7 @@ .commitHash { text-align: center; - font-size: .7em; + font-size: 0.7em; background: var(--g200); color: var(--g600); diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index 433b2df3..7376c6f5 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -31,6 +31,12 @@ export default function Footer() { Chat on Discord + + + + Credits + +
diff --git a/frontend/src/components/Nav/LoginState.module.scss b/frontend/src/components/Nav/LoginState.module.scss index d5df0c14..828ea211 100644 --- a/frontend/src/components/Nav/LoginState.module.scss +++ b/frontend/src/components/Nav/LoginState.module.scss @@ -8,7 +8,3 @@ .avatar { border-radius: 24px; } - -.loginContainer, .user { - margin-right: .5em; -} diff --git a/frontend/src/components/Nav/LoginState.tsx b/frontend/src/components/Nav/LoginState.tsx index dfabbed8..48050bf6 100644 --- a/frontend/src/components/Nav/LoginState.tsx +++ b/frontend/src/components/Nav/LoginState.tsx @@ -11,7 +11,7 @@ import GitHubLoginButton from "../GitHubLoginButton" import styles from "./LoginState.module.scss" export type Props = { - onChange: (user: api.AnonymousUser | api.User) => void, + onChange: (user: api.AnonymousUser | api.User) => void } export default function LoginState({ onChange }: Props) { @@ -39,11 +39,11 @@ export default function LoginState({ onChange }: Props) { height={24} priority />} - {user.name} + {/*{user.username}*/} } else { - return
+ return
} diff --git a/frontend/src/components/Nav/Nav.module.scss b/frontend/src/components/Nav/Nav.module.scss index 35af138d..e8cb11de 100644 --- a/frontend/src/components/Nav/Nav.module.scss +++ b/frontend/src/components/Nav/Nav.module.scss @@ -8,15 +8,19 @@ align-items: center; justify-content: space-between; flex-wrap: wrap; - gap: .5em; + gap: 0.5em; - padding: 1vh 0.5vw; + padding: 4px; background: var(--g400); border-bottom: 1px solid var(--g600); font-size: 90%; font-weight: 600; + + @media screen and (min-width: 800px) and (min-height: 800px) { + padding: 8px; + } } .item { @@ -26,7 +30,7 @@ height: 100%; padding: 0 8px; - opacity: 1.0; + opacity: 1; &:hover { opacity: 0.8; @@ -36,7 +40,7 @@ .logo { @extend .item; - margin: 0 .5vw; + margin: 0 0.5vw; padding: 0; svg { @@ -46,11 +50,30 @@ /* Frog blinks when you load the page or stop hovering */ &:not(:hover) { @keyframes blink { - 0% { transform: scaleY(1.0); opacity: 1 } - 25% { transform: scaleY(0.1); opacity: 0 } - 50% { transform: scaleY(1.0); opacity: 1 } - 75% { transform: scaleY(0.1); opacity: 0 } - 100% { transform: scaleY(1.0); opacity: 1 } + 0% { + transform: scaleY(1); + opacity: 1; + } + + 25% { + transform: scaleY(0.2); + opacity: 0; + } + + 50% { + transform: scaleY(1); + opacity: 1; + } + + 75% { + transform: scaleY(0.2); + opacity: 0; + } + + 100% { + transform: scaleY(1); + opacity: 1; + } } :global(.frog_svg__pupilR), @@ -58,7 +81,7 @@ :global(.frog_svg__eyeR), :global(.frog_svg__eyeL) { transform-origin: 0 7px; - animation: blink .4s 2s ease; + animation: blink 0.4s 2s ease; @media (prefers-reduced-motion) { animation: none; @@ -70,7 +93,3 @@ .grow { flex: 1; } - -.link:hover, .linkActive { - opacity: 0.8; -} diff --git a/frontend/src/components/Nav/Nav.tsx b/frontend/src/components/Nav/Nav.tsx index c499c0d2..63f6b701 100644 --- a/frontend/src/components/Nav/Nav.tsx +++ b/frontend/src/components/Nav/Nav.tsx @@ -4,11 +4,11 @@ import Frog from "./frog.svg" import LoginState, { Props as LoginStateProps } from "./LoginState" import styles from "./Nav.module.scss" -const onUserChangeNop = (_user: any) => {} +const onUserChangeNop: LoginStateProps["onChange"] = _user => {} export type Props = { - children?: React.ReactNode, - onUserChange?: LoginStateProps["onChange"], + children?: React.ReactNode + onUserChange?: LoginStateProps["onChange"] } export default function Nav({ children, onUserChange }: Props) { @@ -19,7 +19,7 @@ export default function Nav({ children, onUserChange }: Props) { - {children ?? + {children ?? New scratch } diff --git a/frontend/src/components/ScoreBadge.module.scss b/frontend/src/components/ScoreBadge.module.scss new file mode 100644 index 00000000..df3e3991 --- /dev/null +++ b/frontend/src/components/ScoreBadge.module.scss @@ -0,0 +1,36 @@ +.badge { + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 0.5em; + + min-width: 2em; + height: 2em; + + font-size: 0.8em; + font-weight: 400; + padding: 0.4em 0.8em; + + border-radius: 999px; + color: var(--a800); + background: var(--a100); + + // circle when using icons + &.error, + &.match { + padding: 0; + } + + &.error { + background: #b93636; + } + + &.match { + background: var(--accent); + } +} + +.icon { + width: 1.2em; +} diff --git a/frontend/src/components/ScoreBadge.tsx b/frontend/src/components/ScoreBadge.tsx new file mode 100644 index 00000000..a141ba9f --- /dev/null +++ b/frontend/src/components/ScoreBadge.tsx @@ -0,0 +1,24 @@ +import { AlertIcon, CheckIcon } from "@primer/octicons-react" +import classNames from "classnames" + +import styles from "./ScoreBadge.module.scss" + +export type Props = { + score: number +} + +export default function ScoreBadge({ score }: Props) { + if (score === -1) { + return
+ +
+ } else if (score === 0) { + return
+ +
+ } else { + return
+ {score} +
+ } +} diff --git a/frontend/src/components/Scratch/AboutScratch.module.scss b/frontend/src/components/Scratch/AboutScratch.module.scss new file mode 100644 index 00000000..53aa585b --- /dev/null +++ b/frontend/src/components/Scratch/AboutScratch.module.scss @@ -0,0 +1,100 @@ +.container { + display: flex; + flex-direction: column; + gap: 1em; + cursor: default; + + height: 100%; +} + +.rule { + border: 0; + background: var(--g300); + height: 1px; +} + +.label { + display: block; + margin: 0; + padding: 10px; + + color: var(--g1000); + font-size: 0.9em; + font-weight: 500; + + small { + color: var(--g1000); + font-size: 0.8em; + } +} + +.horizontalField { + display: flex; + align-items: center; + + .label { + width: 100px; + } +} + +.scratchLinkContainer { + display: inline-flex; + align-items: center; + justify-content: center; + height: 100%; + + color: var(--g800); +} + +.scratchLink { + cursor: pointer; + color: var(--g1400); + + &:hover { + color: var(--g1700); + text-decoration: underline; + } +} + +.scratchLinkByText { + padding-left: 6px; + padding-right: 4px; +} + +.textArea { + flex: 1; + padding: 10px; + + border: 1px solid transparent; + border-radius: 4px; + background: transparent; + outline: none !important; + + color: var(--g1800); + resize: none; + + width: 100%; + + cursor: auto; + user-select: text; + + &::-webkit-input-placeholder { + color: var(--g500); + } + + &:not(:disabled) { + cursor: auto; + transition: border-color 0.15s; + + &:hover, + &:active { + border-color: var(--g300); + } + } +} + +.grow { + display: flex; + flex-direction: inherit; + flex: 1; +} diff --git a/frontend/src/components/Scratch/AboutScratch.tsx b/frontend/src/components/Scratch/AboutScratch.tsx new file mode 100644 index 00000000..7dc4468e --- /dev/null +++ b/frontend/src/components/Scratch/AboutScratch.tsx @@ -0,0 +1,54 @@ +import Link from "next/link" + +import * as api from "../../lib/api" +import UserLink from "../user/UserLink" + +import styles from "./AboutScratch.module.scss" + +function ScratchLink({ slug }: { slug: string }) { + const { scratch } = api.useScratch(slug) + + return + + + {scratch.name || "Untitled scratch"} + + + by + + +} + +export type Props = { + scratch: api.Scratch + setScratch?: (scratch: Partial) => void +} + +export default function AboutScratch({ scratch, setScratch }: Props) { + return
+
+
+

Owner

+ +
+ {scratch.parent &&
+

Fork of

+ +
} +
+ +
+ + {setScratch || scratch.description ?
+

Description

+