Skip to content

Public tribe-name leaderboard tab - #4746

Merged
evanpelle merged 2 commits into
mainfrom
tribe-leaderboard
Jul 28, 2026
Merged

Public tribe-name leaderboard tab#4746
evanpelle merged 2 commits into
mainfrom
tribe-leaderboard

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

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

Adds a third "Tribes" tab to the leaderboard modal, ranking purchased bot
tribe names by rolling 30-day player reach.

GET /leaderboard/tribes is 1-based, 50 per page, capped at page 2, and
carries no total or hasMore — so fetchTribeLeaderboard() requests page 1
and only asks for page 2 when page 1 came back exactly full, then appends.
A page-2 failure keeps page 1 rather than erroring the whole tab. No pager
UI: with a two-page cap and a board that starts well under one page, it
would be ceremony.

Unlike LeaderboardClanTable there are no sortable columns. 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+.

Window bounds are plain strings, and formatWindowDate builds a Date from
the parsed parts rather than new Date(str): a date-only string parses as
UTC midnight, so the clan modal's toLocaleDateString pattern renders the
PREVIOUS day west of UTC ("Jun 26" for 2026-06-27 in America/Los_Angeles).
It also rejects rollover (2026-13-45 would silently become next February)
and returns null on anything unparseable so the caption drops instead of
showing "Invalid Date" — the same tolerance boostExpiresAt needed.

The buyer renders as a secondary line under the tribe name via
<player-name>, which already owns the username-or-publicId fallback and the
verified badge. An Owner column would push Player Reach, the metric the
board ranks by, off screen on mobile. Note that player-name ignores
nameClass unless onNameClick is also set, so the name links to the profile
modal via the same openFromLeaderboard handoff the ranked tab uses.

playerReach is impressions, not distinct people: copy says "Appeared in
games with N players", never "seen by N people".

Deploy after infra#474 — until it ships the tab renders its error state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1cf827b6-4309-4f77-9f15-3b5bca2ce4a1

📥 Commits

Reviewing files that changed from the base of the PR and between bc24810 and 562e2b5.

📒 Files selected for processing (1)
  • resources/lang/en.json

Walkthrough

Adds a tribes leaderboard API contract and paginated fetcher, a LitElement tribe table with loading and error states, and a new Tribes tab in the leaderboard modal with translations and tests.

Changes

Tribe leaderboard

Layer / File(s) Summary
Leaderboard contract and fetch
src/core/ApiSchemas.ts, src/client/Api.ts, tests/ApiSchemas.test.ts, tests/client/ApiTribeLeaderboard.test.ts
Defines validated tribe leaderboard entries and responses, fetches up to two pages, and tests pagination and failure handling.
Tribe table rendering
src/client/components/leaderboard/LeaderboardTribeTable.ts, tests/client/LeaderboardTribeTable.test.ts
Adds loading, error, empty, and data views with date formatting, rank styling, reach bars, and owner profile links.
Leaderboard modal integration
src/client/LeaderboardModal.ts, resources/lang/en.json, tests/client/LeaderboardModal.test.ts
Adds the Tribes tab, table loading and preloading, refresh display, localized text, and modal test wiring.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant LeaderboardModal
  participant LeaderboardTribeTable
  participant Api
  Player->>LeaderboardModal: select Tribes tab
  LeaderboardModal->>LeaderboardTribeTable: ensureLoaded()
  LeaderboardTribeTable->>Api: fetchTribeLeaderboard()
  Api-->>LeaderboardTribeTable: tribe leaderboard response
  LeaderboardTribeTable-->>LeaderboardModal: render tribe rows
Loading

Suggested labels: UI/UX

Suggested reviewers: celant

Poem

Tribes climb the board in bright array,
Two pages chart their reach each day.
Tabs now bloom with stats anew,
Dates stay local, and errors retry too.
A leaderboard tale in colors displayed!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding a public tribe-name leaderboard tab.
Description check ✅ Passed The description is directly about the new Tribes leaderboard tab and related implementation details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@evanpelle evanpelle added this to the v33 milestone Jul 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/client/LeaderboardModal.test.ts (1)

68-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep API pagination logic out of this modal mock.

The modal test’s fetch stub always returns an empty page, so this duplicate pagination implementation never loads page 2. It can also drift from TRIBE_LEADERBOARD_PAGE_SIZE and the real response validation. Return a small fixed response here and keep pagination coverage in tests/client/ApiTribeLeaderboard.test.ts.

Proposed simplification
-    fetchTribeLeaderboard: vi.fn(async () => {
-      const load = async (page: number) => {
-        const url = new URL(`${getApiBase()}/leaderboard/tribes`);
-        url.searchParams.set("page", String(page));
-        const res = await fetch(url.toString(), {
-          headers: { Accept: "application/json" },
-        });
-        return res.ok ? res.json() : false;
-      };
-      const first = await load(1);
-      if (first === false || first.tribes.length < 50) return first;
-      const second = await load(2);
-      if (second === false) return first;
-      return { ...first, tribes: [...first.tribes, ...second.tribes] };
-    }),
+    fetchTribeLeaderboard: vi.fn(async () => ({
+      windowDays: 30,
+      start: "2026-06-27",
+      end: "2026-07-27",
+      tribes: [],
+    })),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/client/LeaderboardModal.test.ts` around lines 68 - 84, Replace the
duplicated pagination logic inside the fetchTribeLeaderboard mock with a small
fixed leaderboard response suitable for the modal test. Remove the local load
function and page 2 handling; keep pagination and response-validation coverage
in ApiTribeLeaderboard tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@resources/lang/en.json`:
- Line 935: Update the tribes tooltip translation keys around
tribes_games_tooltip and the related players tooltip to use ICU pluralization
compatible with IntlMessageFormat, producing singular text when count is 1 and
plural text otherwise. Add matching pluralized keys to every other language file
that already defines these translations, preserving each locale’s existing
wording.

---

Nitpick comments:
In `@tests/client/LeaderboardModal.test.ts`:
- Around line 68-84: Replace the duplicated pagination logic inside the
fetchTribeLeaderboard mock with a small fixed leaderboard response suitable for
the modal test. Remove the local load function and page 2 handling; keep
pagination and response-validation coverage in ApiTribeLeaderboard tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a4c69b68-46af-465b-bc41-ecdd423621b6

📥 Commits

Reviewing files that changed from the base of the PR and between 7a9e3bd and bc24810.

📒 Files selected for processing (9)
  • resources/lang/en.json
  • src/client/Api.ts
  • src/client/LeaderboardModal.ts
  • src/client/components/leaderboard/LeaderboardTribeTable.ts
  • src/core/ApiSchemas.ts
  • tests/ApiSchemas.test.ts
  • tests/client/ApiTribeLeaderboard.test.ts
  • tests/client/LeaderboardModal.test.ts
  • tests/client/LeaderboardTribeTable.test.ts

Comment thread resources/lang/en.json
"ratio": "Ratio",
"refresh_time": "Refreshed every 1 hour",
"title": "Leaderboard",
"tribes_games_tooltip": "Featured in {count} games",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate files =="
git ls-files | rg '(^|/)resources/lang/en\.json$|translateText|ICU|plural|intl-messageformat' || true

echo
echo "== context around reported keys =="
if [ -f resources/lang/en.json ]; then
  nl -ba resources/lang/en.json | sed -n '925,945p'
fi

echo
echo "== usages of reported keys =="
rg -n '"tribes_games_tooltip"|"tribes_reach_value_tooltip"|tribes_games_tooltip|tribes_reach_value_tooltip' . -S || true

echo
echo "== search translation implementation snippets =="
rg -n "translateText|messageformat|Intl|plural|{.*count" . -S --glob '!node_modules' --glob '!dist' --glob '!build' | head -200 || true

Repository: openfrontio/OpenFrontIO

Length of output: 280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "-- language file keys around reported ranges --"
awk 'NR>=925 && NR<=945 { printf "%6d  %s\n", NR, $0 }' resources/lang/en.json

echo
echo "-- usages of reported keys --"
grep -RInE 'tribes_games_tooltip|tribes_reach_value_tooltip' . || true

echo
echo "-- translation implementation snippets --"
grep -RInE 'function translateText|const .*(translate|translation)|Intl\.|messageformat|plural|{.*count|interpolat' . \
  --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build \
  | head -200 || true

echo
echo "-- deterministic probe for simple interpolations without plural handler --"
python3 - <<'PY'
text = "Featured in {count} games"
for count in [1, 2, 10]:
    print(count, text.replace("{count}", str(count)))
PY

Repository: openfrontio/OpenFrontIO

Length of output: 26730


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "-- Translator/Utils locations --"
grep -RInE 'export .*translateText|function translateText|new IntlMessageFormat|formatValue|IntlMessageFormat' src tests --include='*.ts' --include='*.tsx' | head -80 || true

echo
echo "-- translation system test around plural behavior --"
awk 'NR>=1 && NR<=220 { printf "%6d  %s\n", NR, $0 }' tests/client/LeaderboardModal.test.ts 2>/dev/null || true
awk 'NR>=1 && NR<=220 { printf "%6d  %s\n", NR, $0 }' tests/Translationsystem.test.ts 2>/dev/null || true

echo
echo "-- package intl-messageformat dependency and availability check --"
if [ -f package.json ]; then jq '.dependencies, .devDependencies' package.json | grep -E '(intl-messageformat|`@formatjs/icu-messageformat-parser`)' -C1 || true; fi
if [ -f package-lock.json ]; then jq '.packages["node_modules/intl-messageformat"].version, .packages["node_modules/@formatjs/icu-messageformat-parser"].version' package-lock.json || true; fi
node - <<'JS'
try {
  const intl = require('intl-messageformat');
  console.log('intl-messageformat loaded', intl.IntlMessageFormat.version || '(no version)');
  const formatter = new intl.IntlMessageFormat('{count, plural, one {# game} other {# games}}');
  for (const n of [1, 2, 10]) console.log(n, formatter.format({ count: n }));
} catch (error) {
  console.log('intl-messageformat unavailable:', error.message);
}
JS

Repository: openfrontio/OpenFrontIO

Length of output: 10092


Use plurals for the tribe tooltip counts.

count === 1 currently renders as “1 games” / “1 players”. Use ICU pluralization with IntlMessageFormat, and add the same plural keys to the other language files that already use plural forms.

Also applies to: 940

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/lang/en.json` at line 935, Update the tribes tooltip translation
keys around tribes_games_tooltip and the related players tooltip to use ICU
pluralization compatible with IntlMessageFormat, producing singular text when
count is 1 and plural text otherwise. Add matching pluralized keys to every
other language file that already defines these translations, preserving each
locale’s existing wording.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jul 28, 2026
Drop the "one player who saw the same name in three games counts three
times" sentence; the tooltip now just states what the number is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@evanpelle
evanpelle merged commit 296d65f into main Jul 28, 2026
13 checks passed
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management Jul 28, 2026
@evanpelle
evanpelle deleted the tribe-leaderboard branch July 28, 2026 01:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant