diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 70ad7007..fa04b169 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -452,7 +452,7 @@ export async function submitCode( const details = payloadData?.details as Record | undefined; const sub_id = (details?.id as number) || 0; - console.log("Submission successful with sub_id", sub_id); + console.log("Submission successful with details", details); return { sub_id }; diff --git a/frontend/src/pages/home/Home.test.tsx b/frontend/src/pages/home/Home.test.tsx index 0b0a44e0..5b5a15f4 100644 --- a/frontend/src/pages/home/Home.test.tsx +++ b/frontend/src/pages/home/Home.test.tsx @@ -9,9 +9,20 @@ import { vi, expect, it, describe, beforeEach } from "vitest"; // Mock the API hook vi.mock("../../lib/hooks/useApi"); +// Mock react-syntax-highlighter to avoid ESM issues +vi.mock("react-syntax-highlighter", () => ({ + default: ({ children }: { children: string }) =>
{children}
, + Prism: ({ children }: { children: string }) =>
{children}
, +})); + +vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({ + vscDarkPlus: {}, +})); + // Mock utility functions vi.mock("../../lib/date/utils", () => ({ getTimeLeft: vi.fn(() => "2 days 5 hours remaining"), + isExpired: vi.fn(() => false), })); vi.mock("../../lib/utils/ranking", () => ({ @@ -41,6 +52,7 @@ describe("Home", () => { const mockHookReturn = { data: null, loading: true, + hasLoaded: false, error: null, errorStatus: null, call: mockCall, @@ -52,13 +64,18 @@ describe("Home", () => { renderWithProviders(); - expect(screen.getByText(/Summoning/i)).toBeInTheDocument(); + // 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(); }); it("shows error message", () => { const mockHookReturn = { data: null, loading: false, + hasLoaded: true, error: "Something went wrong", errorStatus: 500, call: mockCall, @@ -78,6 +95,7 @@ describe("Home", () => { const mockHookReturn = { data: null, loading: false, + hasLoaded: true, error: "Network error", errorStatus: null, call: mockCall, @@ -117,6 +135,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -143,6 +162,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -202,6 +222,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -240,6 +261,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -273,6 +295,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -313,6 +336,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -351,6 +375,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -399,6 +424,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -439,6 +465,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -478,6 +505,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -528,6 +556,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -568,6 +597,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -610,6 +640,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, @@ -649,6 +680,7 @@ describe("Home", () => { const mockHookReturn = { data: mockData, loading: false, + hasLoaded: true, error: null, errorStatus: null, call: mockCall, diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index 0310b9d2..5d5640a3 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -6,10 +6,16 @@ import { DialogContent, DialogTitle, Typography, + List, + ListItemButton, + ListItemText, + Stack, + Chip, } from "@mui/material"; +import CodeIcon from "@mui/icons-material/Code"; import Grid from "@mui/material/Grid"; import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { fetchLeaderboardSummaries } from "../../api/api"; import { fetcherApiCallback } from "../../lib/hooks/useApi"; import { ErrorAlert } from "../../components/alert/ErrorAlert"; @@ -18,6 +24,8 @@ import Loading from "../../components/common/loading"; import { ConstrainedContainer } from "../../components/app-layout/ConstrainedContainer"; import MarkdownRenderer from "../../components/markdown-renderer/MarkdownRenderer"; import quickStartMarkdown from "./quick-start.md?raw"; +import { isExpired, getTimeLeft } from "../../lib/date/utils"; +import { ColoredSquare } from "../../components/common/ColoredSquare"; interface TopUser { rank: number; @@ -41,7 +49,9 @@ interface LeaderboardSummaries { export default function Home() { const [searchParams] = useSearchParams(); + const navigate = useNavigate(); const [isQuickStartOpen, setIsQuickStartOpen] = useState(false); + const [isLeaderboardSelectOpen, setIsLeaderboardSelectOpen] = useState(false); const useBeta = searchParams.has("use_beta"); const forceRefresh = searchParams.has("force_refresh"); @@ -59,6 +69,14 @@ export default function Home() { }, [call, useBeta, forceRefresh]); const leaderboards = data?.leaderboards || []; + const activeLeaderboards = leaderboards.filter( + (lb) => !isExpired(lb.deadline) + ); + + const handleLeaderboardSelect = (id: number) => { + setIsLeaderboardSelectOpen(false); + navigate(`/leaderboard/${id}/editor`); + }; return ( @@ -66,29 +84,122 @@ export default function Home() { Leaderboards - - + + + + + + {/* Leaderboard Selection Dialog */} + setIsLeaderboardSelectOpen(false)} + maxWidth="sm" + fullWidth + sx={{ + "& .MuiDialog-paper": { + maxHeight: { xs: "80vh", sm: "70vh" }, + }, + }} + > + Select an active leaderboard + + {activeLeaderboards.length > 0 ? ( + + {activeLeaderboards.map((lb) => ( + handleLeaderboardSelect(lb.id)} + sx={{ + py: 1, + px: { xs: 1.5, sm: 2 }, + borderBottom: "1px solid", + borderColor: "divider", + "&:last-child": { borderBottom: "none" }, + }} + > + + + {lb.name} + + } + secondary={getTimeLeft(lb.deadline)} + slotProps={{ + primary: { + fontWeight: 500, + fontSize: { xs: "0.9rem", sm: "0.95rem" }, + component: "div", + }, + secondary: { + fontSize: "0.8rem", + }, + }} + /> + + ))} + + ) : ( + + + No active leaderboards available. + + + )} + + + + + + setIsQuickStartOpen(false)} maxWidth="md" fullWidth > - Submit Your First Kernel + Submit Your First Kernel via cli tool! diff --git a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx index 3a480208..e26ef832 100644 --- a/frontend/src/pages/leaderboard/LeaderboardEditor.tsx +++ b/frontend/src/pages/leaderboard/LeaderboardEditor.tsx @@ -30,6 +30,7 @@ import { ErrorAlert } from "../../components/alert/ErrorAlert"; import MarkdownRenderer from "../../components/markdown-renderer/MarkdownRenderer"; import { SubmissionMode } from "../../lib/types/mode"; import { useAuthStore } from "../../lib/store/authStore"; +import { isExpired, toDateUtc } from "../../lib/date/utils"; import SubmissionHistorySection from "./components/submission-history/SubmissionHistorySection"; import { useThemeStore } from "../../lib/store/themeStore"; import { @@ -237,8 +238,10 @@ export default function LeaderboardEditor() { if (!code.trim()) return false; // Only enable if editor has been modified if (!isEditorDirty) return false; + // Disable if deadline has passed + if (data?.deadline && isExpired(data.deadline)) return false; return true; - }, [code, gpuType, mode, isEditorDirty]); + }, [code, gpuType, mode, isEditorDirty, data?.deadline]); if (loading) return ; if (error) return ; @@ -268,8 +271,12 @@ export default function LeaderboardEditor() { {data.name} - - Submit your Python code + + {isExpired(data.deadline) ? "Ended" : "Ends in"} {toDateUtc(data.deadline)} UTC diff --git a/kernelboard/lib/mocks/mock_submission.py b/kernelboard/lib/mocks/mock_submission.py index b222e70a..d6cc2c84 100644 --- a/kernelboard/lib/mocks/mock_submission.py +++ b/kernelboard/lib/mocks/mock_submission.py @@ -492,7 +492,7 @@ def create_mock_submission( # Return same format as real cluster API return http_success( message="submission success, please refresh submission history", - data={"submission_id": submission_id}, + data={"details": {"id": submission_id}}, ) except Exception as e: