{"id":51559280,"url":"https://github.com/osuakatsuki/bancho-web.ts","last_synced_at":"2026-07-10T09:01:14.853Z","repository":{"id":369021342,"uuid":"1287834658","full_name":"osuAkatsuki/bancho-web.ts","owner":"osuAkatsuki","description":"A clean, modern web frontend for bancho.py","archived":false,"fork":false,"pushed_at":"2026-07-03T07:02:48.000Z","size":136,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-07-03T09:07:03.493Z","etag":null,"topics":["catch","catchthebeat","fruits","javascript","osu","osugame","osumania","private","react","server","taiko","tailwind","tailwindcss","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/osuAkatsuki.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-03T03:38:45.000Z","updated_at":"2026-07-03T07:02:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/osuAkatsuki/bancho-web.ts","commit_stats":null,"previous_names":["osuakatsuki/bancho-web.ts"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/osuAkatsuki/bancho-web.ts","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osuAkatsuki%2Fbancho-web.ts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osuAkatsuki%2Fbancho-web.ts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osuAkatsuki%2Fbancho-web.ts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osuAkatsuki%2Fbancho-web.ts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/osuAkatsuki","download_url":"https://codeload.github.com/osuAkatsuki/bancho-web.ts/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/osuAkatsuki%2Fbancho-web.ts/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35326963,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["catch","catchthebeat","fruits","javascript","osu","osugame","osumania","private","react","server","taiko","tailwind","tailwindcss","typescript"],"created_at":"2026-07-10T09:01:10.169Z","updated_at":"2026-07-10T09:01:14.839Z","avatar_url":"https://github.com/osuAkatsuki.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# bancho-web\n\nA clean, modern web frontend for [bancho.py](https://github.com/osuAkatsuki/bancho.py),\nbuilt with React + TypeScript. It's an SPA driven entirely by bancho.py's\n**v2 api** — no database access, no server-side rendering.\n\n![](https://img.shields.io/badge/react-18-61dafb) ![](https://img.shields.io/badge/typescript-5-3178c6) ![](https://img.shields.io/badge/tailwindcss-3-38bdf8)\n\n## Features\n\n- **Registration \u0026 sign in** — website account creation (backed by bancho.py's\n  v2 accounts/sessions apis) with optional captcha (reCAPTCHA, hCaptcha or\n  Turnstile); important since bancho.py disables in-game registration by default\n- **Home** — server stats (online/registered players) \u0026 \"how to connect\" guide\n- **Leaderboards** — per-mode rankings (vanilla/relax/autopilot) with\n  performance/score/accuracy/playcount sorts, country filtering, avatars,\n  clan tags \u0026 pagination\n- **Player profiles** — per-mode stats with global/country ranks, level bar,\n  grade counts, userpage, live \"now playing\" status, best/recent scores and\n  most played maps with beatmap thumbnails\n- **Beatmap pages** — map info with cover art, difficulty stats, and per-mode\n  leaderboards with grades, mods \u0026 FC markers\n- **Clans** — clan listing \u0026 clan pages with member roles\n- **Player search** — debounced live search in the navbar\n- **Settings** — avatar upload for signed-in players (requires bancho.py\n  with `PUT /v2/players/{id}/avatar`)\n\n## Stack\n\n| Layer     | Choice                                        |\n| --------- | --------------------------------------------- |\n| Build     | [Vite 5](https://vitejs.dev)                  |\n| UI        | React 18 + TypeScript (strict)                |\n| Styling   | Tailwind CSS 3                                |\n| Routing   | React Router 6                                |\n| Data      | TanStack Query 5 (caching, retries, refetch)  |\n\n## Development\n\nYou'll need Node.js 18+ and a bancho.py instance to talk to.\n\n```bash\nnpm install\nnpm run dev        # http://localhost:5173\n```\n\nbancho.py routes its api by Host header (`api.{DOMAIN}`), so the dev server\nproxies `/api/*` to your local instance with the Host header rewritten.\nConfigure via `.env` (see `.env.example`):\n\n```bash\nBANCHO_API_TARGET=http://127.0.0.1:10000  # where bancho.py listens\nBANCHO_DOMAIN=cmyui.xyz                   # builds the api.{DOMAIN} Host header\n```\n\nOther scripts:\n\n```bash\nnpm run build            # typecheck + production build into dist/\nnpm run typecheck        # typecheck only\nnpm run preview          # serve the production build locally\nnpm run storybook        # component workshop at http://localhost:6006\nnpm run build-storybook  # static storybook build\n```\n\n## Design system\n\nThe brand theme (near-black neutral palette + crimson accent) lives in\n`tailwind.config.js` as Tailwind color tokens (`canvas`, `surface`,\n`surface-2/3`, `line`, `muted`, `accent`, `accent-2`). Shared primitives\nin `src/components/ui/` keep spacing \u0026 shape consistent:\n\n- `Card` — the standard surface container (default `px-5 py-4` padding,\n  or edge-to-edge for tables \u0026 media)\n- `PageHeader` — page title + description block\n- `PillTabs` — the segmented pill-tab control (mode switchers, sorts, tabs)\n\nRun **Storybook** to iterate on components and the theme in isolation —\nthe `Design/Theme` story documents the palette, type scale, spacing\nconventions and radii.\n\n## Configuration\n\nAll `VITE_*` variables are baked in at build time (see `.env.example`):\n\n| Variable                | Purpose                                | Default             |\n| ----------------------- | -------------------------------------- | ------------------- |\n| `VITE_APP_NAME`         | Server name shown in navbar \u0026 titles   | `bancho.py`         |\n| `VITE_BANCHO_DOMAIN`    | Domain in \"how to connect\" instructions | `cmyui.xyz`        |\n| `VITE_API_BASE_URL`     | Base url for api requests              | `/api`              |\n| `VITE_AVATARS_BASE_URL` | Base url for avatars (`a.{DOMAIN}`)    | `https://a.cmyui.xyz` |\n| `VITE_DISCORD_INVITE`   | Discord invite shown in the footer (keep in sync with bancho.py's `DISCORD_INVITE`) | empty = hidden |\n\n## Deployment\n\nBuild the static bundle and serve `dist/` from any web server. The SPA needs:\n\n1. **History fallback** — unknown paths must serve `index.html`\n2. **An api origin** — keep the default `/api` and proxy it to bancho.py\n   (recommended): sessions ride an http-only, `SameSite=Lax` cookie, which\n   Just Works when everything stays same-origin. (Splitting the api onto\n   another origin requires CORS with `Access-Control-Allow-Credentials` and\n   an explicit `Access-Control-Allow-Origin` — avoid unless you need it.)\n\n```nginx\n# http context: rate limits for the authentication endpoints.\n# registration is limited strictly to prevent automated account\n# creation; logins are limited to slow down credential stuffing.\nlimit_req_zone $binary_remote_addr zone=bancho_registrations:10m rate=1r/m;\nlimit_req_zone $binary_remote_addr zone=bancho_logins:10m rate=10r/m;\nlimit_req_zone $binary_remote_addr zone=bancho_avatar_uploads:10m rate=10r/m;\n\nserver {\n    listen 443 ssl;\n    server_name osu.example.com;\n\n    ssl_certificate     /path/to/fullchain.pem;\n    ssl_certificate_key /path/to/privkey.pem;\n    ssl_ciphers \"EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:@SECLEVEL=1\";\n\n    # bancho.py resolves client ips from these headers (used for\n    # geolocation during registration \u0026 for rate limiting fairness)\n    proxy_set_header Host api.example.com;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\n    # ---- security headers ----\n    # csp: allowlist of what pages may load/execute; limits the blast\n    # radius of xss. keep exactly ONE Content-Security-Policy line —\n    # the one matching your captcha provider — and replace\n    # a.example.com with your avatar host. roll out with\n    # Content-Security-Policy-Report-Only first if you've customized\n    # the frontend.\n\n    # no captcha:\n    add_header Content-Security-Policy \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://a.example.com https://assets.ppy.sh https://b.ppy.sh https://flagcdn.com; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'\" always;\n\n    # cloudflare turnstile:\n    #add_header Content-Security-Policy \"default-src 'self'; script-src 'self' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://a.example.com https://assets.ppy.sh https://b.ppy.sh https://flagcdn.com; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'\" always;\n\n    # google recaptcha:\n    #add_header Content-Security-Policy \"default-src 'self'; script-src 'self' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; frame-src https://www.google.com/recaptcha/; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://a.example.com https://assets.ppy.sh https://b.ppy.sh https://flagcdn.com; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'\" always;\n\n    # hcaptcha:\n    #add_header Content-Security-Policy \"default-src 'self'; script-src 'self' https://hcaptcha.com https://*.hcaptcha.com; frame-src https://hcaptcha.com https://*.hcaptcha.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; img-src 'self' data: https://a.example.com https://assets.ppy.sh https://b.ppy.sh https://flagcdn.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'\" always;\n\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;\n\n    location / {\n        root /srv/bancho-web/dist;\n        try_files $uri /index.html;\n    }\n\n    location = /api/v2/accounts {\n        limit_req zone=bancho_registrations burst=3 nodelay;\n        limit_req_status 429;\n        rewrite ^/api/(.*)$ /$1 break;\n        proxy_pass http://127.0.0.1:10000;\n    }\n\n    location = /api/v2/sessions {\n        limit_req zone=bancho_logins burst=5 nodelay;\n        limit_req_status 429;\n        rewrite ^/api/(.*)$ /$1 break;\n        proxy_pass http://127.0.0.1:10000;\n    }\n\n    # avatar uploads are up to 2MB (nginx's default body limit is 1MB)\n    location ~ ^/api/v2/players/\\d+/avatar$ {\n        limit_req zone=bancho_avatar_uploads burst=5 nodelay;\n        limit_req_status 429;\n        client_max_body_size 4m;\n        rewrite ^/api/(.*)$ /$1 break;\n        proxy_pass http://127.0.0.1:10000;\n    }\n\n    location /api/ {\n        rewrite ^/api/(.*)$ /$1 break;\n        proxy_pass http://127.0.0.1:10000;\n    }\n}\n```\n\n## Project structure\n\n```\nsrc/\n├── lib/\n│   ├── api/          # typed api client (http envelope, types, endpoints)\n│   ├── gamemodes.ts  # mode ids \u003c-\u003e vanilla/relax/autopilot helpers\n│   ├── mods.ts       # mod bitmask -\u003e acronyms\n│   ├── grades.ts     # grade display (XH/X/SH/S/A/...)\n│   ├── rankedStatus.ts\n│   ├── format.ts     # numbers, accuracy, playtime, relative time\n│   └── assets.ts     # avatar/cover/flag url builders\n├── components/       # shared components (Navbar, ModeSwitcher, badges, ...)\n└── pages/            # one component per route\n```\n\nTo consume a new api endpoint: add its response shape to `lib/api/types.ts`,\na fetcher to `lib/api/client.ts`, then use it with `useQuery` in a component.\n\n## External assets\n\n- **Avatars** are served by your bancho.py deployment (`a.{DOMAIN}`), with a\n  built-in placeholder fallback.\n- **Beatmap covers/thumbnails** load from osu!'s public cdn\n  (`assets.ppy.sh` / `b.ppy.sh`) by set id.\n- **Country flags** load from [flagcdn.com](https://flagcdn.com).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fosuakatsuki%2Fbancho-web.ts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fosuakatsuki%2Fbancho-web.ts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fosuakatsuki%2Fbancho-web.ts/lists"}