Skip to content
Open
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
27 changes: 18 additions & 9 deletions frontend/src/components/app-layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,30 @@ interface Props {
children: ReactNode;
}

// Navbar height constants (MUI default Toolbar heights)
const NAVBAR_HEIGHT = { xs: 56, sm: 64, md: 64 };

export default function AppLayout({ children }: Props) {
return (
<Box display="flex" flexDirection="column" minHeight="100vh">
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100vh",
overflow: "hidden",
}}
>
<NavBar />
{/* main content */}
{/* main content - fills space between navbar and footer */}
<Box
component="main"
sx={{
flexGrow: 1,
pt: 4,
pb: 2,
// Account for fixed navbar height. Fixed navbar height removes it
// from the document flow. We push the main content down so it isn't
// covered by the navbar.
mt: { xs: 4, sm: 5, md: 6 },
flex: 1,
display: "flex",
flexDirection: "column",
overflow: "hidden",
// Account for fixed navbar height
mt: { xs: `${NAVBAR_HEIGHT.xs}px`, sm: `${NAVBAR_HEIGHT.sm}px`, md: `${NAVBAR_HEIGHT.md}px` },
}}
>
{children}
Expand Down
285 changes: 240 additions & 45 deletions frontend/src/pages/leaderboard/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,41 @@ import {
Box,
Card,
CardContent,
Divider,
IconButton,
Stack,
styled,
Tab,
Tabs,
Tooltip,
Typography,
} from "@mui/material";
import Grid from "@mui/material/Grid";
import { useEffect, useState } from "react";
import { fetchCodes, fetchLeaderBoard } from "../../api/api";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchLeaderBoard } from "../../api/api";
import { fetcherApiCallback } from "../../lib/hooks/useApi";
import { isExpired, toDateUtc } from "../../lib/date/utils";
import RankingsList from "./components/RankingLists";
import CodeBlock from "../../components/codeblock/CodeBlock";
import { ErrorAlert } from "../../components/alert/ErrorAlert";
import { useParams, useSearchParams } from "react-router-dom";
import Loading from "../../components/common/loading";
import { ConstrainedContainer } from "../../components/app-layout/ConstrainedContainer";
import { SubmissionMode } from "../../lib/types/mode";
import { useAuthStore } from "../../lib/store/authStore";
import SubmissionHistorySection from "./components/submission-history/SubmissionHistorySection";
import LeaderboardSubmit from "./components/LeaderboardSubmit";

export const CardTitle = styled(Typography)(() => ({
fontSize: "1.5rem",
fontSize: "1.1rem",
fontWeight: "bold",
}));

const DEFAULT_SIDEBAR_WIDTH = 320;
const MIN_SIDEBAR_WIDTH = 200;
const MAX_SIDEBAR_WIDTH = 800;
const COLLAPSED_WIDTH = 10;

const TAB_KEYS = ["rankings", "reference", "submission"] as const;
type TabKey = (typeof TAB_KEYS)[number];

Expand All @@ -54,7 +63,7 @@ function TabPanel(props: {
aria-labelledby={`leaderboard-tab-${index}`}
{...other}
>
{value === TAB_KEYS[index] && <Box sx={{ pt: 2 }}>{children}</Box>}
{value === TAB_KEYS[index] && <Box sx={{ pt: 1 }}>{children}</Box>}
</div>
);
}
Expand All @@ -80,6 +89,57 @@ export default function Leaderboard() {
const [refreshFlag, setRefreshFlag] = useState(false);
const triggerRefresh = () => setRefreshFlag((f) => !f);

// Resizable sidebar state
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
const [isCollapsed, setIsCollapsed] = useState(false);
const [prevWidth, setPrevWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
const isResizing = useRef(false);

const startResizing = useCallback(() => {
if (!isCollapsed) {
isResizing.current = true;
}
}, [isCollapsed]);

const stopResizing = useCallback(() => {
isResizing.current = false;
}, []);

const resize = useCallback((e: MouseEvent) => {
if (isResizing.current && !isCollapsed) {
const newWidth = e.clientX;
if (newWidth >= MIN_SIDEBAR_WIDTH && newWidth <= MAX_SIDEBAR_WIDTH) {
setSidebarWidth(newWidth);
}
}
}, [isCollapsed]);

const toggleCollapse = useCallback(() => {
if (isCollapsed) {
setIsCollapsed(false);
setSidebarWidth(prevWidth);
} else {
setPrevWidth(sidebarWidth);
setIsCollapsed(true);
setSidebarWidth(COLLAPSED_WIDTH);
}
}, [isCollapsed, sidebarWidth, prevWidth]);

// Double-click to reset to default width
const handleDoubleClick = useCallback(() => {
setIsCollapsed(false);
setSidebarWidth(DEFAULT_SIDEBAR_WIDTH);
}, []);

useEffect(() => {
window.addEventListener("mousemove", resize);
window.addEventListener("mouseup", stopResizing);
return () => {
window.removeEventListener("mousemove", resize);
window.removeEventListener("mouseup", stopResizing);
};
}, [resize, stopResizing]);

useEffect(() => {
const current = searchParams.get("tab");
if (current !== tab) {
Expand All @@ -97,62 +157,196 @@ export default function Leaderboard() {
if (loading) return <Loading />;
if (error) return <ErrorAlert status={errorStatus} message={error} />;

const descriptionText = (text: string) => (
<Typography component="div" sx={{ whiteSpace: "pre-line" }}>
{text}
</Typography>
);

const toDeadlineUTC = (raw: string) => `ended (${toDateUtc(raw)}) UTC`;

const info_items = [
{ title: "Deadline", content: <span>{toDeadlineUTC(data.deadline)}</span> },
{ title: "Language", content: <span>{data.lang}</span> },
{ title: "GPU types", content: <span>{data.gpu_types.join(", ")}</span> },
{ title: "Deadline", content: toDeadlineUTC(data.deadline) },
{ title: "Language", content: data.lang },
{ title: "GPU Types", content: data.gpu_types.join(", ") },
];

return (
<ConstrainedContainer>
<Box>
<h1>{data.name}</h1>
{/* Header info cards shown above tabs */}
<Grid container spacing={2} marginBottom={2}>
{info_items.map((info, idx) => (
<Grid size={{ xs: 12, md: 4 }} key={idx}>
<Card>
<CardContent>
<CardTitle>{info.title}</CardTitle>
{info.content}
</CardContent>
</Card>
</Grid>
))}
</Grid>
<Grid marginBottom={2}>
<Card>
<CardContent>
<CardTitle fontWeight="bold">Description</CardTitle>
{descriptionText(data.description)}
</CardContent>
</Card>
</Grid>
<Box
sx={{
display: "flex",
flex: 1,
width: "100%",
overflow: "hidden",
}}
>
{/* Left Sidebar - independent scroll */}
<Box
sx={{
width: sidebarWidth,
minWidth: isCollapsed ? COLLAPSED_WIDTH : MIN_SIDEBAR_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
overflow: "hidden",
transition: isResizing.current ? "none" : "width 0.2s ease",
}}
>
{/* Fixed Tabs Header */}
<Box
sx={{
borderBottom: 1,
borderColor: "divider",
flexShrink: 0,
px: 3,
}}
>
<Typography variant="h6" fontWeight="bold" gutterBottom>
{data.name}
</Typography>

<Divider sx={{ my: 2 }} />

{/* Info Items */}
<Stack spacing={2}>
{info_items.map((info, idx) => (
<Box key={idx}>
<Typography
variant="caption"
color="text.secondary"
sx={{ textTransform: "uppercase", letterSpacing: 0.5, fontSize: "0.65rem" }}
>
{info.title}
</Typography>
<Typography variant="body2" sx={{ fontSize: "0.8rem" }}>{info.content}</Typography>
</Box>
))}
</Stack>

<Divider sx={{ my: 2 }} />

{/* Tab navigation */}
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
{/* Description */}
<Typography
variant="caption"
color="text.secondary"
sx={{ textTransform: "uppercase", letterSpacing: 0.5, fontSize: "0.65rem" }}
>
Description
</Typography>
<Typography
variant="body2"
component="div"
sx={{ whiteSpace: "pre-line", mt: 1, fontSize: "0.8rem" }}
>
{data.description}
</Typography>
</Box>
</Box>

{/* Resizable Divider with Toggle Button - LeetCode style */}
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
position: "relative",
flexShrink: 0,
}}
>
<Box
onMouseDown={startResizing}
onDoubleClick={handleDoubleClick}
sx={{
width: "8px",
height: "100%",
cursor: isCollapsed ? "default" : "col-resize",
backgroundColor: "action.hover",
transition: "background-color 0.2s",
"&:hover": {
backgroundColor: isCollapsed ? "action.hover" : "primary.main",
},
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{/* Toggle Button */}
<Tooltip title={isCollapsed ? "Expand panel" : "Collapse panel"}>
<IconButton
onClick={toggleCollapse}
size="small"
sx={{
position: "absolute",
backgroundColor: "background.paper",
border: 1,
borderColor: "divider",
borderRadius: "50%",
width: 24,
height: 24,
zIndex: 10,
"&:hover": {
backgroundColor: "action.hover",
},
}}
>
{isCollapsed ? (
<ChevronRightIcon sx={{ fontSize: 16 }} />
) : (
<ChevronLeftIcon sx={{ fontSize: 16 }} />
)}
</IconButton>
</Tooltip>
</Box>
</Box>

{/* Main Content Area - independent scroll */}
<Box
sx={{
flex: 1,
display: "flex",
flexDirection: "column",
minWidth: 0,
overflow: "hidden",
}}
>
{/* Fixed Tabs Header */}
<Box
sx={{
borderBottom: 1,
borderColor: "divider",
flexShrink: 0,
px: 3,
}}
>
<Tabs
value={tab}
onChange={(_, v: TabKey) => setTab(v)}
aria-label="Leaderboard Tabs"
variant="scrollable"
scrollButtons
allowScrollButtonsMobile
centered
sx={{
minHeight: 36,
mt: 0,
"& .MuiTab-root": {
fontSize: "0.8rem",
minHeight: 36,
py: 0.5,
px: 1.5,
textTransform: "none",
},
}}
>
<Tab label="Rankings" value="rankings" {...a11yProps(0)} />
<Tab label="Reference" value="reference" {...a11yProps(1)} />
<Tab label="Submission" value="submission" {...a11yProps(2)} />
</Tabs>
</Box>

{/* Scrollable Tab Content */}
<Box
sx={{
flex: 1,
overflowY: "auto",
overflowX: "hidden",
px: 3,
py: 1,
}}
>

{/* Ranking Tab */}
<TabPanel value={tab} index={0}>
<Box>
Expand Down Expand Up @@ -234,7 +428,8 @@ export default function Leaderboard() {
</Card>
)}
</TabPanel>
</Box>
</Box>
</ConstrainedContainer>
</Box>
);
}
Loading