Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FC } from "react";

import WithServerComponentErrorBoundary from "@/components/server_component_error_boundary";
import ServerLeaderboardApi from "@/services/api/leaderboard/leaderboard.server";
import { LeaderboardType } from "@/types/scoring";
import { LeaderboardDetails, LeaderboardType } from "@/types/scoring";

import ProjectLeaderboardClient from "./project_leaderboard_client";

Expand All @@ -14,25 +14,43 @@ type Props = {
isQuestionSeries?: boolean;
};

function sortLeaderboards(
leaderboards: LeaderboardDetails[]
): LeaderboardDetails[] {
return [...leaderboards].sort((a, b) => {
const orderA = a.display_config?.display_order ?? 0;
const orderB = b.display_config?.display_order ?? 0;
return orderA - orderB;
});
}

const ProjectLeaderboard: FC<Props> = async ({
projectId,
leaderboardType,
isQuestionSeries,
userId,
}) => {
const leaderboardDetails = (
await ServerLeaderboardApi.getProjectLeaderboard(
projectId,
leaderboardType
? new URLSearchParams({ score_type: leaderboardType })
: null
)
)?.[0]; // This grabs only the first serialized leaderboard, requires work!

if (!leaderboardDetails || !leaderboardDetails.entries.length) {
const params = leaderboardType
? new URLSearchParams({ score_type: leaderboardType })
: null;
const leaderboards = await ServerLeaderboardApi.getProjectLeaderboard(
projectId,
params
);

if (!leaderboards || leaderboards.length === 0) {
return null;
}

const leaderboardsWithEntries = leaderboards.filter(
(lb) => lb.entries.length > 0
);
if (leaderboardsWithEntries.length === 0) {
return null;
}

const sortedLeaderboards = sortLeaderboards(leaderboardsWithEntries);

const t = await getTranslations();

const leaderboardTitle = isQuestionSeries
Expand All @@ -41,7 +59,7 @@ const ProjectLeaderboard: FC<Props> = async ({

return (
<ProjectLeaderboardClient
leaderboardDetails={leaderboardDetails}
leaderboards={sortedLeaderboards}
leaderboardTitle={leaderboardTitle}
isQuestionSeries={isQuestionSeries}
userId={userId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,48 @@ import { useState } from "react";
import InfoToggle from "@/components/ui/info_toggle";
import SectionToggle from "@/components/ui/section_toggle";
import Switch from "@/components/ui/switch";
import TabBar from "@/components/ui/tab_bar";
import { LeaderboardDetails } from "@/types/scoring";

import ProjectLeaderboardTable from "./project_leaderboard_table";

type Props = {
leaderboardDetails: LeaderboardDetails;
leaderboards: LeaderboardDetails[];
leaderboardTitle: string;
isQuestionSeries?: boolean;
userId?: number;
};

function getLeaderboardDisplayName(
leaderboard: LeaderboardDetails,
fallback: string
): string {
return (
leaderboard.display_config?.display_name ?? leaderboard.name ?? fallback
);
}

const ProjectLeaderboardClient = ({
leaderboardDetails,
leaderboards,
leaderboardTitle,
isQuestionSeries,
userId,
}: Props) => {
const t = useTranslations();

const [isAdvanced, setIsAdvanced] = useState(false);
const [activeLeaderboardId, setActiveLeaderboardId] = useState<number>(
leaderboards[0]?.id ?? 0
);

const activeLeaderboard =
leaderboards.find((lb) => lb.id === activeLeaderboardId) ?? leaderboards[0];

if (!activeLeaderboard) {
return null;
}

const hasMultipleLeaderboards = leaderboards.length > 1;

const advancedToggleElement = (
<div className="ml-auto flex items-center gap-2">
Expand All @@ -41,11 +63,16 @@ const ProjectLeaderboardClient = ({
</div>
);

const scoreType = leaderboardDetails.score_type;
const scoreType = activeLeaderboard.score_type;
const isPeer = scoreType === "peer_tournament";
const isSpotPeer = scoreType === "spot_peer_tournament";
const showExplainer = isPeer || isSpotPeer;

const tabOptions = leaderboards.map((lb) => ({
value: lb.id,
label: getLeaderboardDisplayName(lb, t("leaderboard")),
}));

return (
<SectionToggle
title={leaderboardTitle}
Expand All @@ -55,16 +82,25 @@ const ProjectLeaderboardClient = ({
return advancedToggleElement;
}}
>
{!!leaderboardDetails.prize_pool && (
{hasMultipleLeaderboards && (
<div className="border-b border-gray-300 bg-gray-0 p-2 dark:border-gray-300-dark dark:bg-gray-0-dark">
<TabBar
options={tabOptions}
value={activeLeaderboardId}
onChange={setActiveLeaderboardId}
/>
</div>
)}
{!!activeLeaderboard.prize_pool && (
<div className="border-b border-gray-300 bg-mint-300 py-2 text-center font-medium text-mint-700 dark:border-gray-300-dark dark:bg-mint-800 dark:text-mint-300">
{t("prizePool") + ": "}
<span className="font-bold text-mint-800 dark:text-mint-200">
${leaderboardDetails.prize_pool.toLocaleString()}
${activeLeaderboard.prize_pool.toLocaleString()}
</span>
</div>
)}
<ProjectLeaderboardTable
leaderboardDetails={leaderboardDetails}
leaderboardDetails={activeLeaderboard}
userId={userId}
isAdvanced={isAdvanced}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";
import { isNil } from "lodash";
import { useTranslations } from "next-intl";
import { FC, useMemo, useState } from "react";
import { FC, useCallback, useMemo, useState } from "react";

import enMessages from "@/../../messages/en.json";
import Button from "@/components/ui/button";
import { LeaderboardDetails } from "@/types/scoring";
import { LeaderboardDetails, LeaderboardDisplayConfig } from "@/types/scoring";

import TableHeader from "./table_header";
import TableRow from "./table_row";
Expand All @@ -25,6 +26,28 @@ const ProjectLeaderboardTable: FC<Props> = ({
}) => {
const t = useTranslations();

const columnRenames = leaderboardDetails.display_config?.column_renames;

const getColumnName = useCallback(
(
translationKey: Parameters<typeof t>[0],
columnRenames?: LeaderboardDisplayConfig["column_renames"]
): string => {
const localizedName = t(translationKey);
if (!columnRenames) {
return localizedName;
}
const englishName = (enMessages as Record<string, unknown>)[
String(translationKey)
];
if (typeof englishName === "string" && columnRenames[englishName]) {
return columnRenames[englishName];
}
return localizedName;
},
[t]
);

const [step, setStep] = useState(paginationStep);
const leaderboardEntries = useMemo(() => {
return isNil(step)
Expand All @@ -50,19 +73,21 @@ const ProjectLeaderboardTable: FC<Props> = ({
<thead>
<tr>
<TableHeader className="sticky left-0 text-left">
{t("rank")}
{getColumnName("rank", columnRenames)}
</TableHeader>
<TableHeader className="sticky left-0 w-0 max-w-[16rem] text-left">
{t("forecaster")}
{getColumnName("forecaster", columnRenames)}
</TableHeader>
<TableHeader className="text-right">
{getColumnName("totalScore", columnRenames)}
</TableHeader>
<TableHeader className="text-right">{t("totalScore")}</TableHeader>
{isAdvanced && (
<>
<TableHeader className=" text-right">
{t("questions")}
{getColumnName("questions", columnRenames)}
</TableHeader>
<TableHeader className="text-right">
{t("coverage")}
{getColumnName("coverage", columnRenames)}
</TableHeader>
</>
)}
Expand All @@ -71,16 +96,16 @@ const ProjectLeaderboardTable: FC<Props> = ({
{isAdvanced && (
<>
<TableHeader className="text-right">
{t("take")}
{getColumnName("take", columnRenames)}
</TableHeader>
<TableHeader className="text-right">
{t("percentPrize")}
{getColumnName("percentPrize", columnRenames)}
</TableHeader>
</>
)}
<TableHeader className=" text-right">
{leaderboardDetails.finalized ? (
t("prize")
getColumnName("prize", columnRenames)
) : (
<UnfinalizedPrizeTooltip />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ const TournamentTimeline: FC<Props> = async ({ tournament }) => {

let leaderboardDetails: LeaderboardDetails | undefined = undefined;
try {
leaderboardDetails = (
await ServerLeaderboardApi.getProjectLeaderboard(
tournament.id,
new URLSearchParams({ primary_only: "true", with_entries: "false" })
)
)?.[0];
const leaderboards = await ServerLeaderboardApi.getProjectLeaderboard(
tournament.id,
new URLSearchParams({ primary_only: "true", with_entries: "false" })
);
leaderboardDetails = leaderboards?.[0];
} catch (error) {
logError(error);
}
Expand Down
6 changes: 3 additions & 3 deletions front_end/src/services/api/leaderboard/leaderboard.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ class LeaderboardApi extends ApiService {

async getProjectLeaderboard(
projectId: number,
endpointParams: URLSearchParams | null = null
params?: URLSearchParams | null
): Promise<LeaderboardDetails[] | null> {
// TODO: make paginated
const params = endpointParams ?? new URLSearchParams();
const url = `/leaderboards/project/${projectId}/${params.toString() ? `?${params.toString()}` : ""}`;
const searchParams = params ?? new URLSearchParams();
const url = `/leaderboards/project/${projectId}/${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
return await this.get<LeaderboardDetails[]>(url);
}

Expand Down
10 changes: 9 additions & 1 deletion front_end/src/types/scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,15 @@ export type MedalRanksEntry = {
| "questions_global";
};

export type LeaderboardDisplayConfig = {
display_name?: string;
column_renames?: Record<string, string>;
display_order?: number;
display_on_project?: boolean;
};

type BaseLeaderboardDetails = {
id: number;
project_id: number;
project_type: MedalProjectType;
project_name: string;
Expand All @@ -114,7 +122,7 @@ type BaseLeaderboardDetails = {
finalized: boolean;
prize_pool: number | null;
max_coverage?: number;
display_config: Record<string, unknown> | null;
display_config: LeaderboardDisplayConfig | null;
};

export type LeaderboardDetails = BaseLeaderboardDetails & {
Expand Down
Loading