Skip to content

Multi-leaderboard display with tabs for projects#4262

Open
ncarazon wants to merge 1 commit intomainfrom
feat/multi-leaderboard-tabs-frontend
Open

Multi-leaderboard display with tabs for projects#4262
ncarazon wants to merge 1 commit intomainfrom
feat/multi-leaderboard-tabs-frontend

Conversation

@ncarazon
Copy link
Contributor

@ncarazon ncarazon commented Feb 6, 2026

Closes #4251

This PR adds support for displaying multiple leaderboards with tab navigation on project pages, along with custom display names and column renaming capabilities.

Multiple Leaderboards with Tabs

Tab 1: "Official Results" with renamed columns (Participant, Final Score)

tab1-official-results

Tab 3: "Pre-DQ Results" with renamed column (Raw Score)

tab3-pre-dq-results

Single Leaderboard - No Tabs (Backward Compatibility)

Bridgewater Tournament: Standard leaderboard display without tabs

single-leaderboard-no-tabs

Summary by CodeRabbit

  • New Features
    • Projects can display multiple leaderboards with tab-based navigation to switch between them.
    • Leaderboard tables support customizable column header display names via configuration.
    • Leaderboards are filtered and ordered for clearer, prioritized display on project pages.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Fetches all project leaderboards, filters out empty ones, sorts by display_config.display_order, and passes the sorted list to the client. Client exposes tabs (when >1) to select an active leaderboard; table rendering uses display_config.column_renames and new leaderboard id/display_config` types.

Changes

Cohort / File(s) Summary
Project-level fetch & server component
front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard.tsx
Fetch all leaderboards for a project, filter out those without entries, sort by display_config.display_order, and pass a leaderboards array downstream instead of a single leaderboard.
Client UI & state
front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_client.tsx
Accepts leaderboards: LeaderboardDetails[], manages activeLeaderboard (default first), renders a TabBar when multiple leaderboards exist, and forwards the active leaderboard to the table.
Table & column customization
front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/index.tsx
Adds column_renames handling via getColumnName (fallback to translations) so column headers reflect display_config.column_renames.
API signature & usages
front_end/src/services/api/leaderboard/leaderboard.shared.ts, front_end/src/app/(main)/(tournaments)/tournament/components/tournament_timeline.tsx
getProjectLeaderboard signature changed to accept optional params?; callers updated to handle returned LeaderboardDetails[] and select first element where needed.
Types
front_end/src/types/scoring.ts
Introduces LeaderboardDisplayConfig (display_name, column_renames, display_order, display_on_project) and adds id: number to BaseLeaderboardDetails; display_config typed as `LeaderboardDisplayConfig

Sequence Diagram

sequenceDiagram
    participant Server as ProjectLeaderboard (server)
    participant API as LeaderboardApi
    participant Client as ProjectLeaderboardClient (client)
    participant Table as ProjectLeaderboardTable

    Server->>API: getProjectLeaderboard(projectId)
    API-->>Server: LeaderboardDetails[] (all leaderboards)
    Server->>Server: filter out leaderboards without entries
    Server->>Server: sort by display_config.display_order
    Server->>Client: pass leaderboards[]
    Client->>Client: set activeLeaderboard = leaderboards[0]
    alt multiple leaderboards
        Client->>Client: render TabBar using display_config.display_name
        Client->>Client: on tab select -> set activeLeaderboard
    end
    Client->>Table: render with activeLeaderboard and display_config
    Table->>Table: apply column_renames for headers
    Table-->>Client: rendered table with possibly renamed columns
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through arrays and tabs with glee,

Names and columns reshaped for all to see,
Sorted by order, entries in line,
Tabs for many, lone view for the fine,
A tiny rabbit cheers: "Leaderboards, shine!"

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Multi-leaderboard display with tabs for projects' clearly and concisely summarizes the main change: adding multi-leaderboard functionality with tab navigation for project pages.
Linked Issues check ✅ Passed The pull request implementation fully meets all requirements from issue #4251: multiple leaderboards display with tabs, custom display names in tab labels, column header customization, backward compatibility for single leaderboards, and proper sorting by display_order.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the PR objectives: multi-leaderboard display implementation, tab navigation, column renaming support, API parameter adjustments, and type definitions required for the feature.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/multi-leaderboard-tabs-frontend

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.

Copy link
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
`@front_end/src/app/`(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/index.tsx:
- Around line 35-39: The current lookup uses the translated label (defaultName =
t(translationKey)) to index columnRenames which breaks in non-English locales;
change the lookup to use the stable translation key (translationKey) when
checking columnRenames (e.g. check columnRenames[translationKey] and if present
return that renamed string, otherwise fall back to the existing behavior of
columnRenames[defaultName] and finally defaultName) so renames are
locale-independent; update the code around defaultName, translationKey and
columnRenames to try translationKey first, then the translated label, then the
untranslated default.

In `@front_end/src/types/scoring.ts`:
- Around line 102-107: The new LeaderboardDisplayConfig field display_on_project
is never used; update the filtering in project_leaderboard.tsx (where it
currently checks entries.length > 0) to also respect config.display_on_project
(treat undefined as true for backward compatibility) so leaderboards with
display_on_project=false are hidden on project pages; reference the
LeaderboardDisplayConfig type and the display_on_project property when locating
the code to change and ensure the filter uses config.display_on_project
alongside entries.length.
🧹 Nitpick comments (2)
front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_table/index.tsx (1)

30-42: columnRenames parameter is redundant — simplify by closing over the outer variable.

getColumnName always receives the same columnRenames from the component scope at every call site. Closing over it directly eliminates the repeated argument and simplifies all 7+ call sites.

♻️ Suggested simplification
- const getColumnName = useCallback(
-   (
-     translationKey: Parameters<typeof t>[0],
-     columnRenames?: LeaderboardDisplayConfig["column_renames"]
-   ): string => {
-     const defaultName = t(translationKey);
-     if (!columnRenames) {
-       return defaultName;
-     }
-     return columnRenames[defaultName] ?? defaultName;
-   },
-   [t]
- );
+ const getColumnName = useCallback(
+   (translationKey: Parameters<typeof t>[0]): string => {
+     const defaultName = t(translationKey);
+     if (!columnRenames) {
+       return defaultName;
+     }
+     return columnRenames[defaultName] ?? defaultName;
+   },
+   [t, columnRenames]
+ );

Then simplify all call sites, e.g.:

- {getColumnName("rank", columnRenames)}
+ {getColumnName("rank")}
front_end/src/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard_client.tsx (1)

94-101: toLocaleString() called without explicit locale — inconsistent with tournament_timeline.

In tournament_timeline.tsx (Line 82), prize_pool is formatted with an explicit locale argument. Here it relies on the browser default, which may produce different formatting for the same user.

♻️ Suggested fix

Pass locale from next-intl for consistent formatting:

+ import { useLocale } from "next-intl";
  ...
+ const locale = useLocale();
  ...
- ${activeLeaderboard.prize_pool.toLocaleString()}
+ ${activeLeaderboard.prize_pool.toLocaleString(locale)}

@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4262-feat-multi-leaderboard-tabs-fr-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:feat-multi-leaderboard-tabs-frontend-fa3fa21
🗄️ PostgreSQL NeonDB branch preview/pr-4262-feat-multi-leaderboard-tabs-fr
Redis Fly Redis mtc-redis-pr-4262-feat-multi-leaderboard-tabs-fr

Details

  • Commit: 333f088e781c7db8b455e6e930cf5f744259bd9d
  • Branch: feat/multi-leaderboard-tabs-frontend
  • Fly App: metaculus-pr-4262-feat-multi-leaderboard-tabs-fr

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

Copy link
Contributor

@elisescu elisescu left a comment

Choose a reason for hiding this comment

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

LGTM

- Update API service to accept URLSearchParams and return LeaderboardDetails[]
- Add LeaderboardDisplayConfig type with display_name, column_renames, display_order
- Sort leaderboards by display_order in server component
- Show TabBar when multiple leaderboards exist, hide for single leaderboard
- Apply display_config.display_name for tab labels with fallback to name
- Support column_renames for custom column header names in table
Copy link
Contributor

@lsabor lsabor left a comment

Choose a reason for hiding this comment

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

Great!
Works well, looks good, code is clean.

Left one non-blocking comment that should at least be considered.

Comment on lines +35 to +38
const defaultName = t(translationKey);
if (!columnRenames) {
return defaultName;
}
Copy link
Contributor

@lsabor lsabor Feb 7, 2026

Choose a reason for hiding this comment

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

Non blocking but there is a bug. We can probably just make this a gh issue.

I noticed that the column rename doesn't work if the users are in non-english locales.

A quick fix for this would be rather than having defaultName be taken from t(translationKey), we can take the defaultName from the translation key in english.

This is ugly, but gets the job done:

import enMessages from "../../../../../../../messages/en.json";

...

  const getColumnName = useCallback(
    (
      translationKey: Parameters<typeof t>[0],
      columnRenames?: LeaderboardDisplayConfig["column_renames"]
    ): string => {
      const getEnglishMessage = (key: string): string | undefined => {
        const value = key
          .split(".")
          .reduce<unknown>(
            (current, segment) =>
              typeof current === "object" && current !== null
                ? (current as Record<string, unknown>)[segment]
                : undefined,
            enMessages as Record<string, unknown>
          );
        return typeof value === "string" ? value : undefined;
      };
      const englishName =
        getEnglishMessage(String(translationKey)) ?? t(translationKey);
      if (!columnRenames || !columnRenames[englishName]) {
        return t(translationKey);
      }
      return columnRenames[englishName];
    },
    [t]
  );

The technologically-simplest solution would be instead to store translation key names in the json on the leaderboard and add the translation keys to the en / es / ... .json files. The annoying part there is that our admins would then need to directly type in the translation keys into the field in the admin panel... I'll leave it to you and/or @elisescu to decide if this is worth doing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-leaderboard display with tabs for projects

3 participants