Skip to content

Commit ad38e3c

Browse files
authored
home: separate active private competitions (#228)
1 parent ecfde1a commit ad38e3c

4 files changed

Lines changed: 147 additions & 3 deletions

File tree

frontend/src/api/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface NewsPost {
5555
export interface LeaderboardSummary {
5656
id: number;
5757
name: string;
58+
visibility?: string;
5859
deadline: string;
5960
gpu_types: string[];
6061
priority_gpu_type: string;

frontend/src/pages/home/Home.test.tsx

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ThemeProvider } from "@mui/material";
44
import { appTheme } from "../../components/common/styles/theme";
55
import Home from "./Home";
66
import * as apiHook from "../../lib/hooks/useApi";
7+
import * as dateUtils from "../../lib/date/utils";
78
import { vi, expect, it, describe, beforeEach } from "vitest";
89

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

6768
// Page structure is visible during loading
6869
expect(screen.getByText("Leaderboards")).toBeInTheDocument();
69-
expect(screen.getByText("Submit your first kernel")).toBeInTheDocument();
7070
// Loading indicator is present
7171
expect(screen.getByRole("progressbar")).toBeInTheDocument();
7272
});
@@ -311,6 +311,116 @@ describe("Home", () => {
311311
expect(screen.getByText("L4")).toBeInTheDocument();
312312
});
313313

314+
it("shows active private competitions in their own section before closed competitions", () => {
315+
vi.mocked(dateUtils.isExpired).mockImplementation((deadline: string | Date) => {
316+
if (deadline instanceof Date) return deadline.getTime() < Date.now();
317+
return deadline === "2024-01-01T00:00:00Z";
318+
});
319+
320+
const mockData = {
321+
leaderboards: [
322+
{
323+
id: 1,
324+
name: "public-competition",
325+
visibility: "public",
326+
deadline: "2025-12-31T23:59:59Z",
327+
gpu_types: ["T4"],
328+
priority_gpu_type: "T4",
329+
top_users: null,
330+
},
331+
{
332+
id: 2,
333+
name: "private-competition",
334+
visibility: "closed",
335+
deadline: "2025-12-31T23:59:59Z",
336+
gpu_types: ["A100"],
337+
priority_gpu_type: "A100",
338+
top_users: null,
339+
},
340+
{
341+
id: 3,
342+
name: "expired-public-competition",
343+
visibility: "public",
344+
deadline: "2024-01-01T00:00:00Z",
345+
gpu_types: ["L4"],
346+
priority_gpu_type: "L4",
347+
top_users: null,
348+
},
349+
],
350+
now: "2025-01-01T00:00:00Z",
351+
};
352+
353+
const mockHookReturn = {
354+
data: mockData,
355+
loading: false,
356+
hasLoaded: true,
357+
error: null,
358+
errorStatus: null,
359+
call: mockCall,
360+
};
361+
362+
(apiHook.fetcherApiCallback as ReturnType<typeof vi.fn>).mockReturnValue(
363+
mockHookReturn,
364+
);
365+
366+
renderWithProviders(<Home />);
367+
368+
expect(screen.getByText("Active Competitions")).toBeInTheDocument();
369+
expect(screen.getByText("Private Competitions")).toBeInTheDocument();
370+
expect(screen.getByText("Closed Competitions")).toBeInTheDocument();
371+
expect(screen.getByText("private-competition")).toBeInTheDocument();
372+
373+
const privateHeading = screen.getByText("Private Competitions");
374+
const closedHeading = screen.getByText("Closed Competitions");
375+
expect(
376+
Boolean(
377+
privateHeading.compareDocumentPosition(closedHeading) &
378+
Node.DOCUMENT_POSITION_FOLLOWING,
379+
),
380+
).toBe(true);
381+
});
382+
383+
it("keeps expired private competitions in the closed competitions section", () => {
384+
vi.mocked(dateUtils.isExpired).mockImplementation((deadline: string | Date) => {
385+
if (deadline instanceof Date) return deadline.getTime() < Date.now();
386+
return deadline === "2024-01-01T00:00:00Z";
387+
});
388+
389+
const mockData = {
390+
leaderboards: [
391+
{
392+
id: 1,
393+
name: "expired-private-competition",
394+
visibility: "closed",
395+
deadline: "2024-01-01T00:00:00Z",
396+
gpu_types: ["H100"],
397+
priority_gpu_type: "H100",
398+
top_users: null,
399+
},
400+
],
401+
now: "2025-01-01T00:00:00Z",
402+
};
403+
404+
const mockHookReturn = {
405+
data: mockData,
406+
loading: false,
407+
hasLoaded: true,
408+
error: null,
409+
errorStatus: null,
410+
call: mockCall,
411+
};
412+
413+
(apiHook.fetcherApiCallback as ReturnType<typeof vi.fn>).mockReturnValue(
414+
mockHookReturn,
415+
);
416+
417+
renderWithProviders(<Home />);
418+
419+
expect(screen.queryByText("Private Competitions")).not.toBeInTheDocument();
420+
expect(screen.getByText("Closed Competitions")).toBeInTheDocument();
421+
expect(screen.getByText("expired-private-competition")).toBeInTheDocument();
422+
});
423+
314424
describe("LeaderboardTile functionality", () => {
315425
it("displays time left correctly", () => {
316426
const mockData = {

frontend/src/pages/home/Home.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ interface TopUser {
3636
interface LeaderboardData {
3737
id: number;
3838
name: string;
39+
visibility?: string;
3940
deadline: string;
4041
gpu_types: string[];
4142
priority_gpu_type: string;
@@ -76,12 +77,23 @@ export default function Home() {
7677
const activeLeaderboards = leaderboards.filter(
7778
(lb) => !isExpired(lb.deadline)
7879
);
80+
const isPrivateCompetition = (lb: LeaderboardData) =>
81+
lb.visibility === "closed";
7982

8083
const activeCompetitions = leaderboards.filter(
81-
(lb) => !isExpired(lb.deadline) && !isBeginnerProblem(lb.name)
84+
(lb) =>
85+
!isExpired(lb.deadline) &&
86+
!isBeginnerProblem(lb.name) &&
87+
!isPrivateCompetition(lb)
8288
);
8389
const beginnerProblems = leaderboards.filter(
84-
(lb) => !isExpired(lb.deadline) && isBeginnerProblem(lb.name)
90+
(lb) =>
91+
!isExpired(lb.deadline) &&
92+
isBeginnerProblem(lb.name) &&
93+
!isPrivateCompetition(lb)
94+
);
95+
const privateCompetitions = leaderboards.filter(
96+
(lb) => !isExpired(lb.deadline) && isPrivateCompetition(lb)
8597
);
8698
const closedCompetitions = leaderboards.filter((lb) =>
8799
isExpired(lb.deadline)
@@ -277,6 +289,25 @@ export default function Home() {
277289
</Box>
278290
)}
279291

292+
{/* Private Competitions */}
293+
{privateCompetitions.length > 0 && (
294+
<Box sx={{ mb: 5 }}>
295+
<Typography variant="h5" component="h2" sx={{ mb: 0.5 }}>
296+
Private Competitions
297+
</Typography>
298+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
299+
Invite-only competitions with dedicated leaderboard pages.
300+
</Typography>
301+
<Grid container spacing={3}>
302+
{privateCompetitions.map((leaderboard) => (
303+
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 4 }} key={leaderboard.id}>
304+
<LeaderboardTile leaderboard={leaderboard} />
305+
</Grid>
306+
))}
307+
</Grid>
308+
</Box>
309+
)}
310+
280311
{/* Closed Competitions */}
281312
{closedCompetitions.length > 0 && (
282313
<Box>

kernelboard/api/leaderboard_summaries.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ def _get_leaderboard_metadata_query():
311311
jsonb_build_object(
312312
'id', l.id,
313313
'name', l.name,
314+
'visibility', l.visibility,
314315
'deadline', l.deadline,
315316
'gpu_types', COALESCE(g.gpu_types, '[]'::jsonb),
316317
'priority_gpu_type', p.gpu_type
@@ -508,6 +509,7 @@ def _get_query():
508509
SELECT jsonb_build_object(
509510
'id', l.id,
510511
'name', l.name,
512+
'visibility', l.visibility,
511513
'deadline', l.deadline,
512514
'gpu_types', COALESCE(g.gpu_types, '[]'::jsonb),
513515
'priority_gpu_type', p.gpu_type,

0 commit comments

Comments
 (0)