296d65f9ad Public tribe-name leaderboard tab (#4746)
Adds a third **Tribes** tab to the leaderboard modal, ranking purchased
bot tribe names by rolling 30-day player reach. Implements the infra
handoff for `GET /leaderboard/tribes`.

> [!IMPORTANT]
> **Merge/deploy after infra#474.** Until that endpoint ships, the tab
renders its error state (verified — it's contained to the tab and
doesn't break the modal).
>
> Separately, the handoff is explicit that the board was **not** meant
to launch alongside names — soft-launch names, let inventory build, then
launch as a "Season 1" moment. The tab is currently unconditional; say
the word and I'll gate it.

## What's here

| | |
|---|---|
| `src/core/ApiSchemas.ts` | `TribeLeaderboardResponseSchema` |
| `src/client/Api.ts` | `fetchTribeLeaderboard()` |
| `src/client/components/leaderboard/LeaderboardTribeTable.ts` | new
`<leaderboard-tribe-table>` |
| `src/client/LeaderboardModal.ts` | third tab; two-tab load/prefetch
logic generalized over `TAB_KEYS` |
| `resources/lang/en.json` | 8 new `leaderboard_modal.*` keys |

## Design notes

**Paging.** The response has no `total` or `hasMore`, so
`fetchTribeLeaderboard()` requests page 1 and only asks for page 2 when
page 1 came back *exactly* 50 long, then appends. A page-2 failure keeps
page 1 rather than erroring the tab. No pager UI — with a two-page cap,
and a board that starts well under one page, it'd be ceremony.

**No sortable columns**, unlike `LeaderboardClanTable`. Its client-side
sort would silently sort only the loaded rows, and the server-supplied
`rank` is the point of the board. Rows render `entry.rank`, not `index +
1`, so page 2 keeps ranks 51+.

**Dates don't go through `new Date(str)`.** A date-only string parses as
UTC midnight, so the clan modal's `new Date(x).toLocaleDateString()`
pattern renders the *previous* day west of UTC. Confirmed by patching
the naive version back in — it fails under `TZ=America/Los_Angeles` with
`expected 'Jun 26' to be 'Jun 27'`. `formatWindowDate` builds from the
parsed parts, rejects rollover (`2026-13-45` would silently become next
February), and returns null on anything unreadable so the caption drops
instead of showing "Invalid Date" — the same tolerance `boostExpiresAt`
needed, which is also why the window bounds are plain `z.string()`.

**Owner** renders as a secondary line under the tribe name via
`<player-name>`, which already owns the `username ?? publicId` fallback
and the verified badge. An Owner column would push Player Reach — the
metric the board ranks by — off screen on mobile. Note `player-name`
ignores `nameClass` unless `onNameClick` is also set (the no-click
branch renders a `<copy-button>` chip), so the name links to the profile
modal via the same `openFromLeaderboard` handoff the ranked tab uses.

**Wording is a product rule:** `playerReach` is impressions, not
distinct people — one player who saw a name in three games counts three
times. Copy says "Appeared in games with N players" / "Featured in N
games", never "seen by N people". Encoded in the tooltips and in
comments on the schema.

## Verification

Full suite (2395 + 287) passing, `tsc`/`eslint`/`prettier` clean.

Driven in the running app against stubbed routes:
- two-page fetch → `pages requested: ["1","2"]`, 62 rows from 50 + 12
- caption `Rolling 30-day window (Jun 27 – Jul 27)`
- owner cases: account username, `null` username falling back to the
public id, bare-name claim showing the verified badge, and a long name
truncating rather than wrapping
- desktop and 420px; error state (endpoint absent) and empty state

Column widths are tuned so Player Reach stays on screen at mobile widths
rather than behind the horizontal scroll the clans tab has.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:38:57 -07:00
2026-07-13 15:22:52 -07:00

OpenFrontIO Logo

OpenFront.io is an online real-time strategy game focused on territorial control and alliance building. Players compete to expand their territory, build structures, and form strategic alliances in various maps based on real-world geography.

This is a fork/rewrite of WarFront.io. Credit to https://github.com/WarFrontIO.

CI Crowdin CLA assistant License: AGPL v3 Assets: CC BY-SA 4.0

License

OpenFront source code is licensed under the GNU Affero General Public License v3.0

Current copyright notices appear in:

  • Footer: "© OpenFront and Contributors"
  • Loading screen: "© OpenFront and Contributors"

Modified versions must preserve these notices in reasonably visible locations.

See the LICENSE for complete requirements.

For asset licensing, see LICENSE-ASSETS.
For license history, see LICENSING.md.

🌟 Features

  • Real-time Strategy Gameplay: Expand your territory and engage in strategic battles
  • Alliance System: Form alliances with other players for mutual defense
  • Multiple Maps: Play across various geographical regions including Europe, Asia, Africa, and more
  • Resource Management: Balance your expansion with defensive capabilities
  • Cross-platform: Play in any modern web browser

📋 Prerequisites

  • npm (v10.9.2 or higher)
  • A modern web browser (Chrome, Firefox, Edge, etc.)

🚀 Installation

  1. Clone the repository

    git clone https://github.com/openfrontio/OpenFrontIO.git
    cd OpenFrontIO
    
  2. Install dependencies

    npm run inst
    

    Do NOT use npm install nor npm i but instead use our npm run inst. It runs the safer npm ci --ignore-scripts to install dependencies exactly according to the versions in package-lock.json and doesn't run scripts. This can prevent being hit by a supply chain attack.

🎮 Running the Game

Development Mode

Run both the client and server in development mode with live reloading:

npm run dev

This will:

  • Start the webpack dev server for the client
  • Launch the game server with development settings
  • Open the game in your default browser (to disable this behavior, set SKIP_BROWSER_OPEN=true in your environment)

Client Only

To run just the client with hot reloading:

npm run start:client

Server Only

To run just the server with development settings:

npm run start:server-dev

Connecting to staging or production backends

Sometimes it's useful to connect to production servers when replaying a game, testing user profiles, purchases, or login flow.

To replay a production game, make sure you're on the same commit that the game you want to replay was executed on, you can find the gitCommit value via https://api.openfront.io/game/[gameId]. Unfinished games cannot be replayed on localhost.

To connect to staging api servers:

npm run dev:staging

To connect to production api servers:

npm run dev:prod

🛠️ Development Tools

  • Format code:

    npm run format
    
  • Lint code:

    npm run lint
    
  • Lint and fix code:

    npm run lint:fix
    
  • Testing

    npm test
    

🏗️ Project Structure

  • /src/client - Frontend game client
  • /src/core - Deterministic game simulation
  • /src/server - Backend game server
  • /resources - Static assets (images, maps, etc.)

🤝 Contributing

Contributions and translations are welcome! See CONTRIBUTING.md for the workflow, the approved-issue process, project governance, and translation info.

S
Description
No description provided
Readme AGPL-3.0
1,020 MiB
Languages
TypeScript 91.4%
GLSL 2.5%
JavaScript 2%
HTML 1.5%
Go 1%
Other 1.5%