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
1 change: 1 addition & 0 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface NewsPost {
export interface LeaderboardSummary {
id: number;
name: string;
visibility?: string;
deadline: string;
gpu_types: string[];
priority_gpu_type: string;
Expand Down
112 changes: 111 additions & 1 deletion frontend/src/pages/home/Home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ThemeProvider } from "@mui/material";
import { appTheme } from "../../components/common/styles/theme";
import Home from "./Home";
import * as apiHook from "../../lib/hooks/useApi";
import * as dateUtils from "../../lib/date/utils";
import { vi, expect, it, describe, beforeEach } from "vitest";

// Mock the API hook
Expand Down Expand Up @@ -66,7 +67,6 @@ describe("Home", () => {

// Page structure is visible during loading
expect(screen.getByText("Leaderboards")).toBeInTheDocument();
expect(screen.getByText("Submit your first kernel")).toBeInTheDocument();
// Loading indicator is present
expect(screen.getByRole("progressbar")).toBeInTheDocument();
});
Expand Down Expand Up @@ -311,6 +311,116 @@ describe("Home", () => {
expect(screen.getByText("L4")).toBeInTheDocument();
});

it("shows active private competitions in their own section before closed competitions", () => {
vi.mocked(dateUtils.isExpired).mockImplementation((deadline: string | Date) => {
if (deadline instanceof Date) return deadline.getTime() < Date.now();
return deadline === "2024-01-01T00:00:00Z";
});

const mockData = {
leaderboards: [
{
id: 1,
name: "public-competition",
visibility: "public",
deadline: "2025-12-31T23:59:59Z",
gpu_types: ["T4"],
priority_gpu_type: "T4",
top_users: null,
},
{
id: 2,
name: "private-competition",
visibility: "closed",
deadline: "2025-12-31T23:59:59Z",
gpu_types: ["A100"],
priority_gpu_type: "A100",
top_users: null,
},
{
id: 3,
name: "expired-public-competition",
visibility: "public",
deadline: "2024-01-01T00:00:00Z",
gpu_types: ["L4"],
priority_gpu_type: "L4",
top_users: null,
},
],
now: "2025-01-01T00:00:00Z",
};

const mockHookReturn = {
data: mockData,
loading: false,
hasLoaded: true,
error: null,
errorStatus: null,
call: mockCall,
};

(apiHook.fetcherApiCallback as ReturnType<typeof vi.fn>).mockReturnValue(
mockHookReturn,
);

renderWithProviders(<Home />);

expect(screen.getByText("Active Competitions")).toBeInTheDocument();
expect(screen.getByText("Private Competitions")).toBeInTheDocument();
expect(screen.getByText("Closed Competitions")).toBeInTheDocument();
expect(screen.getByText("private-competition")).toBeInTheDocument();

const privateHeading = screen.getByText("Private Competitions");
const closedHeading = screen.getByText("Closed Competitions");
expect(
Boolean(
privateHeading.compareDocumentPosition(closedHeading) &
Node.DOCUMENT_POSITION_FOLLOWING,
),
).toBe(true);
});

it("keeps expired private competitions in the closed competitions section", () => {
vi.mocked(dateUtils.isExpired).mockImplementation((deadline: string | Date) => {
if (deadline instanceof Date) return deadline.getTime() < Date.now();
return deadline === "2024-01-01T00:00:00Z";
});

const mockData = {
leaderboards: [
{
id: 1,
name: "expired-private-competition",
visibility: "closed",
deadline: "2024-01-01T00:00:00Z",
gpu_types: ["H100"],
priority_gpu_type: "H100",
top_users: null,
},
],
now: "2025-01-01T00:00:00Z",
};

const mockHookReturn = {
data: mockData,
loading: false,
hasLoaded: true,
error: null,
errorStatus: null,
call: mockCall,
};

(apiHook.fetcherApiCallback as ReturnType<typeof vi.fn>).mockReturnValue(
mockHookReturn,
);

renderWithProviders(<Home />);

expect(screen.queryByText("Private Competitions")).not.toBeInTheDocument();
expect(screen.getByText("Closed Competitions")).toBeInTheDocument();
expect(screen.getByText("expired-private-competition")).toBeInTheDocument();
});

describe("LeaderboardTile functionality", () => {
it("displays time left correctly", () => {
const mockData = {
Expand Down
35 changes: 33 additions & 2 deletions frontend/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface TopUser {
interface LeaderboardData {
id: number;
name: string;
visibility?: string;
deadline: string;
gpu_types: string[];
priority_gpu_type: string;
Expand Down Expand Up @@ -76,12 +77,23 @@ export default function Home() {
const activeLeaderboards = leaderboards.filter(
(lb) => !isExpired(lb.deadline)
);
const isPrivateCompetition = (lb: LeaderboardData) =>
lb.visibility === "closed";

const activeCompetitions = leaderboards.filter(
(lb) => !isExpired(lb.deadline) && !isBeginnerProblem(lb.name)
(lb) =>
!isExpired(lb.deadline) &&
!isBeginnerProblem(lb.name) &&
!isPrivateCompetition(lb)
);
const beginnerProblems = leaderboards.filter(
(lb) => !isExpired(lb.deadline) && isBeginnerProblem(lb.name)
(lb) =>
!isExpired(lb.deadline) &&
isBeginnerProblem(lb.name) &&
!isPrivateCompetition(lb)
);
const privateCompetitions = leaderboards.filter(
(lb) => !isExpired(lb.deadline) && isPrivateCompetition(lb)
);
const closedCompetitions = leaderboards.filter((lb) =>
isExpired(lb.deadline)
Expand Down Expand Up @@ -277,6 +289,25 @@ export default function Home() {
</Box>
)}

{/* Private Competitions */}
{privateCompetitions.length > 0 && (
<Box sx={{ mb: 5 }}>
<Typography variant="h5" component="h2" sx={{ mb: 0.5 }}>
Private Competitions
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Invite-only competitions with dedicated leaderboard pages.
</Typography>
<Grid container spacing={3}>
{privateCompetitions.map((leaderboard) => (
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 4 }} key={leaderboard.id}>
<LeaderboardTile leaderboard={leaderboard} />
</Grid>
))}
</Grid>
</Box>
)}

{/* Closed Competitions */}
{closedCompetitions.length > 0 && (
<Box>
Expand Down
2 changes: 2 additions & 0 deletions kernelboard/api/leaderboard_summaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ def _get_leaderboard_metadata_query():
jsonb_build_object(
'id', l.id,
'name', l.name,
'visibility', l.visibility,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add migration before selecting leaderboard visibility

This query now selects l.visibility, but the repo’s canonical DB schema (tests/data.sql, leaderboard.leaderboard definition) does not include a visibility column, and that schema is loaded by tests/conftest.py and mounted in docker-compose.dev.yml. In those environments /api/leaderboard-summaries will error with column l.visibility does not exist, so this change breaks the default test/dev path unless a schema migration (and fixture update) is added alongside the API change.

Useful? React with 👍 / 👎.

'deadline', l.deadline,
'gpu_types', COALESCE(g.gpu_types, '[]'::jsonb),
'priority_gpu_type', p.gpu_type
Expand Down Expand Up @@ -508,6 +509,7 @@ def _get_query():
SELECT jsonb_build_object(
'id', l.id,
'name', l.name,
'visibility', l.visibility,
'deadline', l.deadline,
'gpu_types', COALESCE(g.gpu_types, '[]'::jsonb),
'priority_gpu_type', p.gpu_type,
Expand Down
Loading