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
4 changes: 2 additions & 2 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export default tseslint.config(
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
'error',
{ allowConstantExport: true },
],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
Expand Down
91 changes: 80 additions & 11 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { User } from "../lib/types/user";

export class APIError extends Error {
status: number;
constructor(message: string, status: number) {
Expand All @@ -16,7 +18,75 @@ export async function fetchAboutInfo(): Promise<string> {
return r.data.message;
}

export async function fetchLeaderBoard(id: string): Promise<any> {
export interface LeaderboardDetail {
deadline: string;
description: string;
name: string;
reference: string;
gpu_types: string[];
rankings: Record<
string,
Array<{
file_name: string;
prev_score: number;
rank: number;
score: number;
user_name: string;
submission_id: number;
}>
>;
}

export interface CodesResponse {
results: Array<{
submission_id: number;
code: string;
}>;
}

export interface NewsPost {
id: string;
title: string;
content: string;
[key: string]: unknown;
}

export interface LeaderboardSummary {
id: number;
name: string;
deadline: string;
gpu_types: string[];
priority_gpu_type: string;
top_users: Array<{ rank: number; score: number; user_name: string }> | null;
}

export interface LeaderboardSummariesResponse {
leaderboards: LeaderboardSummary[];
now: string;
}

export interface UserSubmissionsResponse {
items: Array<{
submission_id: number;
file_name?: string | null;
submitted_at: string;
status?: string | null;
submission_done: boolean;
runs?: Array<{
start_time: string;
end_time: string | null;
mode: string;
passed: boolean;
score: number | null;
meta: Record<string, unknown> | null;
report: Record<string, unknown> | null;
}>;
}>;
total: number;
limit: number;
}

export async function fetchLeaderBoard(id: string): Promise<LeaderboardDetail> {
const start = performance.now();
const res = await fetch(`/api/leaderboard/${id}`);
const fetchTime = performance.now() - start;
Expand All @@ -41,7 +111,7 @@ export async function fetchLeaderBoard(id: string): Promise<any> {
export async function fetchCodes(
leaderboardId: number | string,
submissionIds: (number | string)[],
): Promise<any> {
): Promise<CodesResponse> {
const res = await fetch("/api/codes", {
method: "POST",
headers: {
Expand All @@ -62,7 +132,7 @@ export async function fetchCodes(
return r.data;
}

export async function fetchAllNews(): Promise<any> {
export async function fetchAllNews(): Promise<NewsPost[]> {
const res = await fetch("/api/news");
if (!res.ok) {
const json = await res.json();
Expand All @@ -73,7 +143,7 @@ export async function fetchAllNews(): Promise<any> {
return r.data;
}

export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise<any> {
export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise<LeaderboardSummariesResponse> {
const start = performance.now();
const url = useV1
? "/api/leaderboard-summaries?v1_query"
Expand Down Expand Up @@ -102,7 +172,7 @@ export async function fetchLeaderboardSummaries(useV1: boolean = false): Promise
return r.data;
}

export async function getMe(): Promise<any> {
export async function getMe(): Promise<User> {
const res = await fetch("/api/me");
if (!res.ok) {
const json = await res.json();
Expand All @@ -113,15 +183,14 @@ export async function getMe(): Promise<any> {
return r.data;
}

export async function logout(): Promise<any> {
export async function logout(): Promise<void> {
const res = await fetch("/api/logout");
if (!res.ok) {
const json = await res.json();
const message = json?.message || "Unknown error";
throw new APIError(`Failed to fetch news contents: ${message}`, res.status);
}
const r = await res.json();
return r.data;
await res.json();
}

export async function submitFile(form: FormData) {
Expand All @@ -131,15 +200,15 @@ export async function submitFile(form: FormData) {
});

const text = await resp.text();
let data: any;
let data: Record<string, unknown>;
try {
data = JSON.parse(text);
} catch {
data = { raw: text };
}

if (!resp.ok) {
const msg = data?.detail || data?.message || "Submission failed";
const msg = (data?.detail as string) || (data?.message as string) || "Submission failed";
throw new Error(msg);
}

Expand All @@ -151,7 +220,7 @@ export async function fetchUserSubmissions(
userId: number | string,
page: number = 1,
pageSize: number = 10,
): Promise<any> {
): Promise<UserSubmissionsResponse> {
const offset = (page - 1) * pageSize;
const res = await fetch(
`/api/submissions?leaderboard_id=${leaderboardId}&offset=${offset}&limit=${pageSize}`,
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/common/styles/shared_style.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import styled from "@emotion/styled";
import { Container } from "@mui/material";

// eslint-disable-next-line react-refresh/only-export-components
export const flexRowCenter = {
display: "flex",
alignItems: "center",
gap: 0.5,
};

// eslint-disable-next-line react-refresh/only-export-components
export const flexRowCenterMediumGap = {
display: "flex",
alignItems: "center",
gap: 5,
};

// eslint-disable-next-line react-refresh/only-export-components
export const mediumText = {
fontSize: "1.25rem",
};
Expand Down
13 changes: 7 additions & 6 deletions frontend/src/lib/hooks/useApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useState } from "react";
import { useNavigate } from "react-router-dom";

type Fetcher<T, Args extends any[]> = (...args: Args) => Promise<T>;
type Fetcher<T, Args extends unknown[]> = (...args: Args) => Promise<T>;

export const defaultRedirectMap: Record<number, string> = {
401: "/401",
Expand Down Expand Up @@ -49,7 +49,7 @@ export const defaultRedirectMap: Record<number, string> = {
* ```
* @returns An object with `data`, `loading`, `error`, `errorStatus`, and `call`
*/
export function fetcherApiCallback<T, Args extends any[]>(
export function fetcherApiCallback<T, Args extends unknown[]>(
fetcher: Fetcher<T, Args>,
redirectMap: Record<number, string> = defaultRedirectMap,
) {
Expand All @@ -71,9 +71,10 @@ export function fetcherApiCallback<T, Args extends any[]>(
const result = await fetcher(...params);
setData(result);
return result;
} catch (e: any) {
const status = e.status ? e.status : 0;
const msg = e.message ? e.message : "";
} catch (e: unknown) {
const err = e as { status?: number; message?: string };
const status = err.status ? err.status : 0;
const msg = err.message ? err.message : "";

// set and logging the error if any
setError(status);
Expand All @@ -84,7 +85,7 @@ export function fetcherApiCallback<T, Args extends any[]>(

// navigate to config page if the status is found
// otherwise passes
const redirectPath = redirectMap[e.status];
const redirectPath = redirectMap[err.status ?? 0];
if (redirectPath) {
navigate(redirectPath);
}
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/lib/store/authStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type AuthState = {
inFlight: boolean;
setMe: (me: User | null) => void;
fetchMe: () => Promise<void>;
logoutAndRefresh: () => Promise<any>;
logoutAndRefresh: () => Promise<{ ok: boolean; error?: unknown }>;
};

export const useAuthStore = create<AuthState>((set, get) => ({
Expand All @@ -33,9 +33,9 @@ export const useAuthStore = create<AuthState>((set, get) => ({
inFlight: false,
error: null,
}));
} catch (e: any) {
} catch (e: unknown) {
set((_s) => ({
error: e?.message ?? "Failed to fetch user",
error: e instanceof Error ? e.message : "Failed to fetch user",
loading: false,
inFlight: false,
me: null,
Expand All @@ -48,7 +48,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
await logout();
await get().fetchMe();
return { ok: true };
} catch (e: any) {
} catch (e: unknown) {
return { ok: false, error: e };
}
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/home/components/LeaderboardTile.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Card, CardContent, Chip, Typography } from "@mui/material";
import { Box, Card, CardContent, Chip, type Theme, Typography } from "@mui/material";
import { Link } from "react-router-dom";
import { getMedalIcon } from "../../../components/common/medal.tsx";
import { getTimeLeft } from "../../../lib/date/utils.ts";
Expand All @@ -17,7 +17,7 @@ const styles = {
"&:hover": {
transform: "translateY(-2px)",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
borderColor: (theme: any) => theme.palette.custom.secondary,
borderColor: (theme: Theme) => theme.palette.custom.secondary,
},
padding: 1,
"& .MuiCardContent-root:last-child": {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/leaderboard/Leaderboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let currentAuth: AuthState = { me: null };
vi.mock("../../lib/store/authStore", () => {
return {
// Simulate Zustand's selector pattern
useAuthStore: (selector: any) =>
useAuthStore: (selector: (state: { me: AuthState["me"] }) => unknown) =>
selector({
me: currentAuth.me,
}),
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/leaderboard/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ export default function Leaderboard() {
next.set("tab", tab);
setSearchParams(next, { replace: true });
}
}, [tab]);
}, [tab, searchParams, setSearchParams]);

// Fetch leaderboard data
useEffect(() => {
if (id) call(id);
}, [id]);
}, [id, call]);

// Fetch top user (strongest submission) when rankings are available
// Select from the GPU with the most unique users
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/pages/leaderboard/components/AiTrendChart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import ReactECharts from "echarts-for-react";
import {
Box,
Expand Down Expand Up @@ -45,7 +45,10 @@ export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartPr
const isDark = resolvedMode === "dark";
const textColor = isDark ? "#e0e0e0" : "#333";

const gpuTypes = data?.time_series ? Object.keys(data.time_series) : [];
const gpuTypes = useMemo(
() => (data?.time_series ? Object.keys(data.time_series) : []),
[data?.time_series],
);

useEffect(() => {
if (gpuTypes.length > 0 && !selectedGpuType && data?.time_series) {
Expand Down Expand Up @@ -115,8 +118,8 @@ export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartPr
try {
const result = await fetchAiTrend(leaderboardId);
setData(result);
} catch (err: any) {
setError(err.message || "Failed to load data");
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to load data");
} finally {
setLoading(false);
}
Expand Down Expand Up @@ -203,7 +206,7 @@ export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartPr
}

// Build series for ECharts
const series: any[] = [];
const series: Array<Record<string, unknown>> = [];

Object.entries(selectedData).forEach(([model, dataPoints]) => {
const color = hashStringToColor(model);
Expand Down Expand Up @@ -249,7 +252,7 @@ export default function AiTrendChart({ leaderboardId, rankings }: AiTrendChartPr
},
tooltip: {
trigger: "item",
formatter: (params: any) => {
formatter: (params: { value: [number, number]; data: { gpu_type?: string }; seriesName: string }) => {
const date = new Date(params.value[0]);
const score = formatMicroseconds(params.value[1]);
const gpuType = params.data.gpu_type || "Unknown";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/leaderboard/components/CodeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function CodeDialog({
userName,
problemName,
}: {
code: any;
code: string | null | undefined;
fileName?: string;
isActive?: boolean;
rank?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ vi.mock("../../../api/api", () => ({
// Make AlertBar deterministic and closable
vi.mock("../../../components/alert/AlertBar", () => ({
__esModule: true,
default: ({ notice, onClose }: any) =>
default: ({ notice, onClose }: { notice: { open: boolean; title?: string; message?: string }; onClose: () => void }) =>
notice?.open ? (
<div data-testid="alertbar">
<div data-testid="alertbar-title">{notice.title}</div>
Expand Down Expand Up @@ -79,7 +79,7 @@ function getHiddenFileInput(): HTMLInputElement {
}

async function formDataToObject(fd: FormData) {
const out: Record<string, any> = {};
const out: Record<string, FormDataEntryValue> = {};
fd.forEach((v, k) => (out[k] = v));
return out;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ export default function LeaderboardSubmit({
setOpen(false);
resetForm();
}, 100);
} catch (e: any) {
setStatus({ kind: "error", msg: e?.message || "Submission failed" });
} catch (e: unknown) {
setStatus({ kind: "error", msg: e instanceof Error ? e.message : "Submission failed" });
}
}

Expand Down
Loading
Loading