Skip to content

feat(leaderboard): add the 2v2 ranked ladder as its own tab - #4763

Merged
evanpelle merged 2 commits into
mainfrom
feat/ranked-2v2-leaderboard
Jul 28, 2026
Merged

feat(leaderboard): add the 2v2 ranked ladder as its own tab#4763
evanpelle merged 2 commits into
mainfrom
feat/ranked-2v2-leaderboard

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

Add approved & assigned issue number here:

n/a — owner-initiated, no tracking issue.

Description:

GET /leaderboard/ranked returns both ranked ladders in one payload, but RankedLeaderboardResponseSchema declared only the 1v1 key, so the 2v2 rows were dropped at the parse boundary and the modal rendered a single board. This adds the 2v2 ladder as its own tab.

  • SchemaRankedLeaderboardResponseSchema now declares 2v2 as well, defaulted to [] so a client on this build keeps working against an API deployment that predates the ladder.
  • Independent ladders — the two boards are ranked independently (the same public_id can hold a different elo, record and rank on each), so LeaderboardPlayerList keeps one LadderState per RankedType and renders whichever its new rankedType property selects. That includes the pinned "your ranking" row, which now shows your rank on the ladder you are looking at.
  • One element, one request — both ladder tabs share a single <leaderboard-player-list>; a page fetch fills every ladder, so switching tabs neither refetches nor waits.
  • Per-ladder paginghasMore is tracked per ladder instead of being inferred from the 1v1 page size. The 2v2 board is younger and has no backfill, so it ends several pages before 1v1 does, and the short board stops asking for pages that come back empty.
  • Empty state — an empty ladder renders the no-data state (same markup as the tribes tab) instead of a bare header row. That is what a freshly launched ranked type shows until games are played.
  • Tab overflowo-modal's tablist now wraps. This is the fourth tab, and four uppercase labels overflow a narrow phone with nothing to scroll. The class only takes effect in the overflow case that previously clipped.

tab=players still maps to the 1v1 ladder, so existing #modal=leaderboard&tab=players links and the profile modal's back button are unaffected. The new tab key is players2v2.

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

Not verified locally: this was written in an environment with no npm registry access, so it has not been built, typechecked, or test-run — no screenshots for the same reason. Please let CI (or a local npm test + npm run build-prod) be the first real check.

New tests in tests/client/LeaderboardModal.test.ts cover: a player ranked separately on each ladder, one request filling both, the no-data state for an empty ladder, independent paging, and the tab selecting the 2v2 ladder.

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

DISCORD_USERNAME

🤖 Generated with Claude Code

/leaderboard/ranked returns both ladders in one payload, but the response
schema declared only the 1v1 key, so the 2v2 rows were dropped at the parse
boundary and the modal rendered a single board.

The two ladders are ranked independently — the same public_id can hold a
different elo, record and rank on each — so the player list now keeps one
LadderState per RankedType and renders whichever its `rankedType` property
selects. Both ladder tabs share one element: a page fetch fills every ladder,
so switching tabs neither refetches nor waits.

Paging is per ladder rather than inferred from the 1v1 page size. The 2v2
board is younger and has no backfill, so it ends several pages before 1v1
does; tracking one flag per ladder stops the short board from asking for
pages that will come back empty. An empty ladder now renders the no-data
state instead of a bare header row, which is what a freshly launched ranked
type shows until games are played.

The tablist wraps: this is the fourth tab, and four uppercase labels overflow
a narrow phone with nothing to scroll.

The 2v2 key is defaulted in the schema so a client on this build keeps
working against an API deployment that predates the ladder.

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: b511f907-f39e-48e5-8e6f-97a5edf547c3

📥 Commits

Reviewing files that changed from the base of the PR and between ad2e705 and 535a79c.

📒 Files selected for processing (1)
  • tests/client/LeaderboardModal.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/client/LeaderboardModal.test.ts

Walkthrough

The leaderboard modal adds a 2v2 ranked tab. Player-list state, pagination, current-user rows, empty states, API parsing, translations, layout, and tests now support separate 1v1 and 2v2 ladders.

Changes

Ranked leaderboard support

Layer / File(s) Summary
Ladder contract and modal wiring
src/core/ApiSchemas.ts, src/client/LeaderboardModal.ts, src/client/components/baseComponents/Modal.ts, resources/lang/en.json
Adds the 2v2 response shape and tab, maps tabs to RankedType, preserves ladder context, updates refresh rendering, and allows tabs to wrap.
Independent ladder state and validation
src/client/components/leaderboard/LeaderboardPlayerList.ts, tests/client/LeaderboardModal.test.ts
Stores entries and paging state per ladder, merges 1v1 and 2v2 results independently, renders empty ladders, and tests ranking, pagination, current-user rows, and tab selection.

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

Sequence Diagram(s)

sequenceDiagram
  participant LeaderboardModal
  participant LeaderboardPlayerList
  participant RankedLeaderboardResponseSchema
  LeaderboardModal->>LeaderboardPlayerList: select 1v1 or 2v2 rankedType
  LeaderboardPlayerList->>RankedLeaderboardResponseSchema: parse leaderboard response
  RankedLeaderboardResponseSchema-->>LeaderboardPlayerList: provide ladder-specific entries
  LeaderboardPlayerList-->>LeaderboardModal: render selected ladder and paging state
Loading

Possibly related PRs

Suggested labels: UI/UX

Poem

Two ladders rise side by side,
1v1 and 2v2 stride.
Pages keep each ranking’s place,
Empty boards show quiet space.
Wrapped tabs make room to shine!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a 2v2 ranked ladder tab.
Description check ✅ Passed The description matches the changeset and describes the 2v2 ladder, schema, UI, and tests.
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.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks

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.

@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.

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

163-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

showLadder sets activeTab directly, so the tab-change path is not exercised.

Assigning activeTab skips onTabEnterloadActiveTabDataensureLoaded, which is exactly the code that must not refetch. The "switching tabs must not refetch" assertion at line 591 therefore proves less than it reads. Driving the switch through the modal's tab handler (as the last test does with a real click) would cover the guard on hasLoadedPlayers too.

Also applies to: 578-591

🤖 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 163 - 171, Update the
showLadder helper to switch tabs through the modal’s actual tab-change handler
rather than assigning activeTab directly, using the same event or click path as
the final test. Preserve its waiting for modal.updateComplete and
playerList.updateComplete so the switching-tabs assertion exercises onTabEnter,
loadActiveTabData, and ensureLoaded, including the hasLoadedPlayers guard.
src/client/LeaderboardModal.ts (1)

19-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type the map as partial so the ?? null fallback is honest.

Record<string, RankedType> tells TypeScript every string key yields a RankedType, so PLAYER_TABS[tab] ?? null looks dead to readers and to lint rules, even though at runtime it is the whole point. A partial record makes the lookup type match reality.

♻️ Proposed tweak
-const PLAYER_TABS: Record<string, RankedType> = {
+const PLAYER_TABS: Partial<Record<string, RankedType>> = {
   players: RankedType.OneVOne,
   players2v2: RankedType.TwoVTwo,
 };
🤖 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 `@src/client/LeaderboardModal.ts` around lines 19 - 22, Update the PLAYER_TABS
declaration to use a partial record keyed by string, so lookups for unknown tabs
correctly remain potentially undefined and the existing ?? null fallback is
type-accurate.
🤖 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.

Nitpick comments:
In `@src/client/LeaderboardModal.ts`:
- Around line 19-22: Update the PLAYER_TABS declaration to use a partial record
keyed by string, so lookups for unknown tabs correctly remain potentially
undefined and the existing ?? null fallback is type-accurate.

In `@tests/client/LeaderboardModal.test.ts`:
- Around line 163-171: Update the showLadder helper to switch tabs through the
modal’s actual tab-change handler rather than assigning activeTab directly,
using the same event or click path as the final test. Preserve its waiting for
modal.updateComplete and playerList.updateComplete so the switching-tabs
assertion exercises onTabEnter, loadActiveTabData, and ensureLoaded, including
the hasLoadedPlayers guard.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 27d930b8-4495-4c92-8025-a3e744aa640b

📥 Commits

Reviewing files that changed from the base of the PR and between 74d1978 and ad2e705.

📒 Files selected for processing (6)
  • resources/lang/en.json
  • src/client/LeaderboardModal.ts
  • src/client/components/baseComponents/Modal.ts
  • src/client/components/leaderboard/LeaderboardPlayerList.ts
  • src/core/ApiSchemas.ts
  • tests/client/LeaderboardModal.test.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 28, 2026
The blanket mockResolvedValue also answered the clan and tribe prefetches
fired by the tab click, feeding them ladder-shaped JSON that crashed their
renders after the test ended (unhandled rejection, vitest exit 1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@evanpelle
evanpelle merged commit 43a1228 into main Jul 28, 2026
13 of 14 checks passed
@github-project-automation github-project-automation Bot moved this from Triage to Complete in OpenFront Release Management Jul 28, 2026
@evanpelle
evanpelle deleted the feat/ranked-2v2-leaderboard branch July 28, 2026 23:47
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