Skip to content

Add Ranked 1v1 Leaderboard#3008

Merged
evanpelle merged 15 commits intomainfrom
leaderboards
Feb 1, 2026
Merged

Add Ranked 1v1 Leaderboard#3008
evanpelle merged 15 commits intomainfrom
leaderboards

Conversation

@ryanbarlow97
Copy link
Copy Markdown
Contributor

@ryanbarlow97 ryanbarlow97 commented Jan 23, 2026

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #(issue number)

Description:

@wraith4081 's pr

updates the stats modal to show both 1v1 and clan stats

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

w.o.n

wraith4081 and others added 3 commits January 21, 2026 02:00
## Description:

1v1 ranked leaderboard.
- Renames Stats to Leaderboard
- Merges Clans and Users into the same Modal

<img width="765" height="744" alt="image"
src="https://github.com/user-attachments/assets/e49cb1f6-3551-4045-9681-b847d473ccd3"
/>


## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

wraith4081
#2979)

## Description:

update visuals of current user section in leaderboard modal

## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

wraith4081
@ryanbarlow97 ryanbarlow97 removed their assignment Jan 23, 2026
@ryanbarlow97 ryanbarlow97 added the UI/UX UI/UX changes including assets, menus, QoL, etc. label Jan 23, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 23, 2026

Walkthrough

Replaces the legacy Stats modal with a new Leaderboard feature: adds LeaderboardModal and three leaderboard components, new player/clan/ranked schemas and API fetchers, updates UI entry points/translations/markup, removes StatsModal and its tests, and adds comprehensive tests for the new leaderboard UI.

Changes

Cohort / File(s) Summary
Modal & Core
src/client/LeaderboardModal.ts, src/client/StatsModal.ts
Add LeaderboardModal (tabbed players/clans) and remove legacy StatsModal.
Leaderboard Components
src/client/components/leaderboard/LeaderboardTabs.ts, src/client/components/leaderboard/LeaderboardPlayerList.ts, src/client/components/leaderboard/LeaderboardClanTable.ts
New tab control, virtualized paginated player list with sticky user row, and clan table with sorting, date-range events, loading/error states.
API & Schemas
src/core/ApiSchemas.ts, src/client/Api.ts
Add Player/Ranked/Clan leaderboard Zod schemas and types; add fetchPlayerLeaderboard(page) and fetchClanLeaderboard() with validation and error sentinel handling.
Integration & Entrypoints
src/client/Main.ts, src/client/JoinPrivateLobbyModal.ts, src/client/LangSelector.ts
Register leaderboard components; replace stats-button/stats-modal refs with leaderboard-button/leaderboard-modal; update disabled-unit translation keys.
Nav, Markup & Styles
index.html, src/client/components/DesktopNavBar.ts, src/client/components/MobileNavBar.ts
Replace nav targets/translation keys to leaderboard; adjust Tailwind state-selector syntax and z-index values; insert <leaderboard-modal id="page-leaderboard" ...>.
Localization
resources/lang/en.json
Replace stats_modal with expanded leaderboard_modal keys (title, title_plural, player_stats, ranked_tab, clans_tab, your_ranking, player, elo, etc.).
Dependencies
package.json
Add @lit-labs/virtualizer ^2.1.1.
Tests
tests/client/LeaderboardModal.test.ts, tests/client/StatsModal.test.ts
Add comprehensive tests for leaderboard components; remove old StatsModal tests.

Sequence Diagram(s)

sequenceDiagram
  participant User as Client/User
  participant Modal as LeaderboardModal<br/>rgba(70,130,180,0.5)
  participant PlayerList as LeaderboardPlayerList<br/>rgba(34,139,34,0.5)
  participant ClanTable as LeaderboardClanTable<br/>rgba(255,165,0,0.5)
  participant API as Api.fetch*()<br/>rgba(148,0,211,0.5)
  participant Server as Server/Backend

  User->>Modal: open leaderboard
  Modal->>PlayerList: activate players tab (load)
  PlayerList->>API: fetchPlayerLeaderboard(page)
  API->>Server: GET /leaderboard/ranked?page=...
  Server-->>API: ranked payload / 400
  API-->>PlayerList: parsed RankedLeaderboardResponse / reached_limit
  PlayerList-->>Modal: currentUserEntry / paging state

  User->>Modal: switch to clans tab
  Modal->>ClanTable: activate clans tab (load)
  ClanTable->>API: fetchClanLeaderboard()
  API->>Server: GET /public/clans/leaderboard
  Server-->>API: clans payload
  API-->>ClanTable: parsed ClanLeaderboardResponse
  ClanTable-->>Modal: date-range-change event
  Modal-->>User: render updated UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat/1v1 ranked leaderboard #2928: Nearly identical changes—introduces LeaderboardModal and leaderboard components, replaces StatsModal, updates ApiSchemas, translations, and nav/entrypoints.

Suggested labels

Feature

Poem

🎮 Old stats bowed out; a leaderboard takes stage,
Tabs split players and clans on a fresh new page,
Virtual rows scroll while rankings climb high, 🏆
Tests keep the fetches honest and spry,
Ship the ranks — may the top stay spry!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a ranked 1v1 leaderboard feature. It accurately reflects the primary objective of the PR.
Description check ✅ Passed The description is related to the changeset, mentioning updates to show both 1v1 and clan stats by renaming the stats modal to leaderboard. It covers the core purpose of the changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@VariableVince
Copy link
Copy Markdown
Contributor

@ryanbarlow97 Please remove the json language files other than en.json

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/LeaderboardModal.ts`:
- Around line 179-186: Remove the debug console output and test-only behavior
from the LeaderboardModal by eliminating or gating the block that checks reset
&& this.testRandomUser && this.playerData.length > 0; specifically remove the
console.log call and either delete the entire testRandomUser override of
this.currentUserEntry or wrap it behind a build-time flag (e.g.,
process.env.NODE_ENV === 'test' or a dedicated feature flag) so production
builds never execute the random user selection; update any references to
testRandomUser or add a clear comment if you keep a gated test path.
🧹 Nitpick comments (5)
tests/client/LeaderboardModal.test.ts (2)

98-101: Consider exposing test-friendly accessors.

The heavy use of as unknown as { ... } casts to access private members works but is fragile. If the component's internal structure changes, these tests will fail silently at runtime rather than at compile time.

A cleaner approach: expose a protected or public test interface, or use a helper function that centralizes these casts:

// Helper to reduce duplication and centralize casts
function getModalInternals(modal: LeaderboardModal) {
  return modal as unknown as {
    activeTab: string;
    playerData: Array<Record<string, unknown>>;
    currentUserEntry?: { playerId: string } | null;
    isModalOpen: boolean;
    loadClanLeaderboard: () => Promise<void>;
    loadPlayerLeaderboard: (reset: boolean) => Promise<void>;
  };
}

This is optional since the current approach works.


253-317: Good error and empty state coverage.

The interaction tests cover tab switching, empty data state, and error state well. Consider adding a similar test for the players tab interaction (the current tests only cover clan tab switching explicitly, while player data loading is tested separately in "Player Data Mapping").

src/client/LeaderboardModal.ts (3)

113-124: String-based error detection is fragile.

Checking errorJson?.message?.includes("Page must be between") couples tightly to server error wording. If the message changes, pagination breaks silently.

Consider having the API return an explicit field (e.g., hasMore: false) or a 204 status instead.


227-254: Add explicit handling for "rank" sort.

The default case returns 0, which keeps the original API order. This works, but an explicit case "rank" with a comment would make the intent clearer.

Suggested change
       switch (this.clanSortBy) {
+        case "rank":
+          // Keep original API order (already sorted by rank)
+          return 0;
         case "games":
           aVal = a.games;
           bVal = b.games;

504-509: Use existing player.winRate instead of recalculating.

Line 507 recalculates winRate from player.games and player.wins, but PlayerLeaderboardEntry already has a winRate field computed at fetch time (line 140).

Suggested fix
   private renderPlayerRow(player: PlayerLeaderboardEntry) {
     const isCurrentUser = this.currentUserEntry?.playerId === player.playerId;
     const displayRank = player.rank;
-    const winRate = player.games > 0 ? player.wins / player.games : 0;
+    const winRate = player.winRate;

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Jan 23, 2026
@ryanbarlow97
Copy link
Copy Markdown
Contributor Author

@ryanbarlow97 Please remove the json language files other than en.json

@wraith4081 do you have org yet?

@ryanbarlow97
Copy link
Copy Markdown
Contributor Author

@ryanbarlow97 Please remove the json language files other than en.json

Actually, just checked, this is updating the header of a group, not adding any langs

@VariableVince
Copy link
Copy Markdown
Contributor

@ryanbarlow97 Please remove the json language files other than en.json

Actually, just checked, this is updating the header of a group, not adding any langs

It can still cause some unneeded issue. Hypothetically if tomorrow two updated languages are fetched from Crowdin and merged here, they won't yet contain the new key.

It's best to keep one source of truth to prevent mistakes or bugs or unnecessary extra work :)

@ryanbarlow97
Copy link
Copy Markdown
Contributor Author

@ryanbarlow97 Please remove the json language files other than en.json

Actually, just checked, this is updating the header of a group, not adding any langs

It can still cause some unneeded issue. Hypothetically if tomorrow two updated languages are fetched from Crowdin and merged here, they won't yet contain the new key.

It's best to keep one source of truth to prevent mistakes or bugs or unnecessary extra work :)

its not my branch really, i'd rather @wraith4081 could do it with org

@evanpelle evanpelle changed the title Leaderboards Add Ranked 1v1 Leaderboard Jan 23, 2026
@iiamlewis iiamlewis modified the milestones: v30, v29 Jan 23, 2026
Copy link
Copy Markdown
Member

@Aotumuri Aotumuri left a comment

Choose a reason for hiding this comment

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

Could you revert the changes to the non-English files?
I understand that the tags have changed, so this might be the correct approach from a technical point of view.
However, keeping these files unchanged is important for us, because otherwise the synchronization between Crowdin and this repository will break.

Aotumuri
Aotumuri previously approved these changes Jan 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/LeaderboardModal.ts`:
- Around line 763-769: The date-range rendering in LeaderboardModal (the
clanData block that builds dateRange) uses new Date(...).toLocaleDateString(),
which applies local timezone and can shift dates; change it to treat the server
values as UTC/date-only before formatting (e.g., parse the clanData.start and
clanData.end as UTC or use Date.UTC/getUTCDate/getUTCMonth/getUTCFullYear or
format from an ISO date-only string) and then produce a UTC-based date string
for display so users in other time zones see the intended dates.
- Around line 621-631: The virtualizer is creating a second scroll context by
using scroller: true inside the .virtualizer-container which also has
overflow-y-auto, causing scroll position mismatches; fix by removing scroller:
true from the virtualize() call so the parent .virtualizer-container is the
single scroll owner (adjust any logic in handleScroll() that reads the container
scroll to continue using the parent container), or alternatively remove
overflow-y-auto from .virtualizer-container and give the virtualizer a fixed
height so scroller: true becomes the single scroll container — update the
virtualize call in the render where items: this.playerData, renderItem: (p) =>
this.renderPlayerRow(p) is invoked and ensure handleScroll() and showStickyUser
logic reference the actual scroll container you choose.
♻️ Duplicate comments (1)
src/client/LeaderboardModal.ts (1)

159-166: Remove test-only random user override before merge.

This changes the current user and logs to console in production. Please delete it or gate it behind a test-only flag.

🧹 Nitpick comments (1)
src/client/Api.ts (1)

229-235: Avoid brittle string matching for pagination end.

This relies on message.includes("Page must be between"). If server text changes, the client will never detect end-of-list and will keep erroring. Prefer a structured error code or a dedicated status (e.g., 416/204) and check that instead.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 27, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@index.html`:
- Around line 197-201: Remove the legacy stats-modal element (the custom element
with id "page-stats" and tag name "stats-modal") that was left in the markup
after renaming to leaderboard-modal; locate the unused <stats-modal
id="page-stats" ...> block and delete it so only the new <leaderboard-modal
id="page-leaderboard" ...> remains, ensuring there are no leftover duplicates or
unused IDs in the document.

In `@tests/client/LeaderboardModal.test.ts`:
- Around line 320-344: The test asserts the fetch call too early because
loadActiveTabData() waits on modal.updateComplete before calling fetch; after
dispatching the click on "#clan-leaderboard-tab" you should await the component
updates (await modal.updateComplete and await
awaitChildUpdate("leaderboard-clan-table") or at least a microtask tick) before
asserting global.fetch was called and before further checks of load results;
update the test to move the expect(global.fetch).toHaveBeenCalledWith(...) and
any data checks to after those awaits so they run only after loadActiveTabData()
triggers fetch.
🧹 Nitpick comments (2)
tests/client/LeaderboardModal.test.ts (1)

79-95: Avoid double fetch stubs.

The top-level fetch stub is replaced by the inner beforeEach, so it adds noise. Consider keeping only one.

♻️ Optional cleanup
-beforeEach(() => {
-  vi.stubGlobal(
-    "fetch",
-    vi.fn(async (input: any) => {
-      const url =
-        typeof input === "string" ? input : (input?.url ?? String(input));
-
-      if (url.includes("/public/clans/leaderboard")) {
-        return jsonRes({ start: "...", end: "...", clans: [] });
-      }
-      if (url.includes("/leaderboard/ranked")) {
-        return jsonRes({ "1v1": [] });
-      }
-      return jsonRes({}, false, 404);
-    }),
-  );
-});
src/client/components/leaderboard/LeaderboardTabs.ts (1)

21-71: Avoid derived state for tab classes.

playerClass / clanClass are derived from activeTab but only updated on clicks, so they can drift if the parent sets activeTab. Compute classes from activeTab instead.

♻️ Suggested refactor
-  `@state`()
-  private playerClass = this.getTabClass(this.activeTab === "players");
-  `@state`()
-  private clanClass = this.getTabClass(this.activeTab === "clans");
-
   private handleTabChange(tab: LeaderboardTab) {
+    this.activeTab = tab;
     this.dispatchEvent(
       new CustomEvent<LeaderboardTab>("tab-change", {
         detail: tab,
         bubbles: true,
         composed: true,
       }),
     );
-
-    this.playerClass = this.getTabClass(tab === "players");
-    this.clanClass = this.getTabClass(tab === "clans");
   }

   render() {
+    const playerClass = this.getTabClass(this.activeTab === "players");
+    const clanClass = this.getTabClass(this.activeTab === "clans");
     return html`
       <div
         role="tablist"
         class="flex gap-2 p-1 bg-white/5 rounded-full border border-white/10 mb-6 w-fit mx-auto mt-4"
       >
         <button
           type="button"
           role="tab"
-          class="${this.playerClass}"
+          class="${playerClass}"
           `@click`=${() => this.handleTabChange("players")}
           id="player-leaderboard-tab"
           aria-selected=${this.activeTab === "players"}
         >
           ${translateText("leaderboard_modal.ranked_tab")}
         </button>
         <button
           type="button"
           role="tab"
-          class="${this.clanClass}"
+          class="${clanClass}"
           `@click`=${() => this.handleTabChange("clans")}
           id="clan-leaderboard-tab"
           aria-selected=${this.activeTab === "clans"}
         >
           ${translateText("leaderboard_modal.clans_tab")}
         </button>
       </div>
     `;
   }

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@index.html`:
- Line 143: The Tailwind classes z-40001 and z-99999 are invalid and won't
generate CSS; update the markup where the element with class string containing
"peer in-[.in-game]:hidden z-40001 ... z-[?]" (the sidebar/menu element) to use
arbitrary value bracket syntax: replace z-40001 with z-[40001] (and any z-99999
occurrences elsewhere) so Tailwind recognizes and emits those custom z-index
values.
🧹 Nitpick comments (1)
src/client/LeaderboardModal.ts (1)

12-14: Prefer composition over extending BaseModal.

Extending BaseModal builds a class hierarchy. If possible, wrap a modal instance or delegate behavior so the component stays composable (and use union types for variants).

Copilot AI review requested due to automatic review settings February 1, 2026 15:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@github-project-automation github-project-automation bot moved this from Development to Final Review in OpenFront Release Management Feb 1, 2026
@evanpelle evanpelle merged commit e4280c2 into main Feb 1, 2026
11 checks passed
@evanpelle evanpelle deleted the leaderboards branch February 1, 2026 22:58
@github-project-automation github-project-automation bot moved this from Final Review to Complete in OpenFront Release Management Feb 1, 2026
evanpelle pushed a commit that referenced this pull request Feb 1, 2026
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)

@wraith4081 's pr

updates the stats modal to show both 1v1 and clan stats

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
regression is found:

w.o.n

---------

Co-authored-by: Wraith <54374743+wraith4081@users.noreply.github.com>
Co-authored-by: iamlewis <lewismmmm@gmail.com>
ryanbarlow97 added a commit that referenced this pull request Feb 2, 2026
If this PR fixes an issue, link it below. If not, delete these two
lines.
Resolves #(issue number)

@wraith4081 's pr

updates the stats modal to show both 1v1 and clan stats

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

regression is found:

w.o.n

---------

Co-authored-by: Wraith <54374743+wraith4081@users.noreply.github.com>
Co-authored-by: iamlewis <lewismmmm@gmail.com>
@coderabbitai coderabbitai bot mentioned this pull request Apr 9, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

7 participants