Compare commits

...

8 Commits

29 changed files with 2603 additions and 1261 deletions

View File

@ -1,36 +1,44 @@
/* eslint-env node */
module.exports = { module.exports = {
root: true, env: {
env: { browser: true, es2020: true }, browser: true,
es2021: true,
},
extends: [ extends: [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:react/recommended",
"plugin:react-hooks/recommended", "plugin:react-hooks/recommended",
"prettier" "eslint-config-prettier",
], ],
overrides: [
{
env: { node: true },
files: [".eslintrc.{js,cjs}"],
parserOptions: { sourceType: "script" },
},
],
settings: { react: { version: "detect" } },
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
ecmaVersion: "latest", ecmaVersion: "latest",
sourceType: "module", sourceType: "module",
project: true, project: true,
tsconfigRootDir: __dirname tsconfigRootDir: __dirname,
}, },
plugins: ["react-refresh"], plugins: ["@typescript-eslint", "react", "react-refresh", "prettier"],
rules: { rules: {
"react-refresh/only-export-components": [ "linebreak-style": ["error", "unix"],
"warn", "quotes": ["error", "double"],
{ allowConstantExport: true } "react/react-in-jsx-scope": ["off"],
],
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"warn", "warn",
{ {
"argsIgnorePattern": "^_", "argsIgnorePattern": "^_",
"varsIgnorePattern": "^_", "varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_" "caughtErrorsIgnorePattern": "^_",
} },
] ],
} "prettier/prettier": ["error"],
},
}; };

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
.eslintrc.cjs
pnpm-lock.yaml

View File

@ -1,3 +1,9 @@
{ {
"tabWidth": 4 "bracketSpacing": true,
"endOfLine": "lf",
"printWidth": 120,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "all"
} }

View File

@ -12,17 +12,18 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@ant-design/pro-components": "^2.6.43", "@ant-design/pro-components": "^2.6.43",
"@sentry/react": "^7.90.0", "@sentry/react": "^7.91.0",
"@sentry/vite-plugin": "^2.10.2", "@sentry/vite-plugin": "^2.10.2",
"ace-builds": "^1.32.2", "ace-builds": "^1.32.2",
"antd": "^5.12.4", "antd": "^5.12.5",
"axios": "^1.6.2", "axios": "^1.6.2",
"dayjs": "^1.11.10",
"github-markdown-css": "^5.5.0", "github-markdown-css": "^5.5.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-ace": "^10.1.0", "react-ace": "^10.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-router-dom": "^6.21.0", "react-router-dom": "^6.21.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"rehype-mathjax": "^4.0.3", "rehype-mathjax": "^4.0.3",
"rehype-raw": "^6.1.1", "rehype-raw": "^6.1.1",
@ -35,11 +36,13 @@
"@types/react": "^18.2.45", "@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^6.15.0",
"@vitejs/plugin-react-swc": "^3.5.0", "@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^5.1.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"prettier": "3.0.0", "prettier": "3.0.0",

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,6 @@
import axios from "axios"; import axios from "axios";
axios.defaults.baseURL = axios.defaults.baseURL = import.meta.env.MODE === "production" ? "/api/v1" : "http://127.0.0.1:8000/api/v1";
import.meta.env.MODE === "production"
? "/api/v1"
: "http://127.0.0.1:8000/api/v1";
export interface ResponseWrap<T> { export interface ResponseWrap<T> {
code: number; code: number;
@ -11,18 +8,19 @@ export interface ResponseWrap<T> {
body: T; body: T;
} }
export interface Meta { export interface WithCount<T> {
ID: number; count: number;
CreatedAt: number; data: T[];
UpdatedAt: number;
DeletedAt: number;
} }
export async function send<D, T>( export interface Meta {
api: string, ID: number;
data?: D, CreatedAt: Date;
token?: string, UpdatedAt: Date;
): Promise<T> { DeletedAt: Date;
}
export async function send<D, T>(api: string, data?: D, token?: string): Promise<T> {
try { try {
const resp = await axios.post<ResponseWrap<T>>(api, data, { const resp = await axios.post<ResponseWrap<T>>(api, data, {
headers: { Authorization: token }, headers: { Authorization: token },

View File

@ -50,10 +50,7 @@ export interface UploadResp {
} }
export class ProblemApi { export class ProblemApi {
static async CreateVersion( static async CreateVersion(data: CreateVersionReq, token: string): Promise<void> {
data: CreateVersionReq,
token: string,
): Promise<void> {
return send("/problem/create_version", data, token); return send("/problem/create_version", data, token);
} }

View File

@ -1,4 +1,4 @@
import { Meta, send } from "./base.ts"; import { Meta, send, WithCount } from "./base.ts";
import { SubmissionInfo } from "./submission.ts"; import { SubmissionInfo } from "./submission.ts";
export enum Verdict { export enum Verdict {
@ -37,6 +37,11 @@ export interface StatusInfo {
is_enabled: boolean; is_enabled: boolean;
} }
export interface SubmissionWithPoint {
submission: SubmissionInfo;
point: number;
}
export interface QueryReq { export interface QueryReq {
pid?: number; pid?: number;
uid?: number; uid?: number;
@ -44,11 +49,6 @@ export interface QueryReq {
limit: number; limit: number;
} }
export interface QueryResp {
submission: SubmissionInfo;
point: number;
}
export interface QueryBySubmissionReq { export interface QueryBySubmissionReq {
sid: number; sid: number;
} }
@ -59,21 +59,21 @@ export interface QueryByVersionReq {
limit: number; limit: number;
} }
export interface QueryByVersionResp {
count: number;
status: StatusInfo[];
}
export class StatusApi { export class StatusApi {
static async Query(data: QueryReq): Promise<QueryResp[]> { static async Query(data: QueryReq, token: string): Promise<WithCount<SubmissionWithPoint>> {
return send("/status/query", data); return send("/status/query", data, token);
} }
static async QueryBySubmission( static async QueryBySubmission(data: QueryBySubmissionReq, token: string): Promise<StatusInfo> {
data: QueryBySubmissionReq, return send("/status/query/submission", data, token);
): Promise<StatusInfo> {
return send("/status/query/submission", data);
} }
static async QueryByVersion( static async QueryByVersion(data: QueryByVersionReq, token: string): Promise<WithCount<StatusInfo>> {
data: QueryByVersionReq,
token: string,
): Promise<StatusInfo[]> {
return send("/status/query/version", data, token); return send("/status/query/version", data, token);
} }
} }

View File

@ -1,9 +1,10 @@
import { Meta, send } from "./base.ts"; import { Meta, send } from "./base.ts";
import { UserProfile } from "./user.ts"; import { UserProfile } from "./user.ts";
import { ProblemInfo } from "./problem.ts";
export interface SubmissionInfo { export interface SubmissionInfo {
meta: Meta; meta: Meta;
problem_id: number; problem: ProblemInfo;
user: UserProfile; user: UserProfile;
language: string; language: string;
code: string; code: string;

View File

@ -1,5 +1,11 @@
import { Meta, send } from "./base.ts"; import { Meta, send } from "./base.ts";
export enum UserRole {
Admin = 30,
User = 20,
Guest = 10,
}
export interface UserReq { export interface UserReq {
username?: string; username?: string;
password?: string; password?: string;

View File

@ -1,35 +1,20 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { Navigate } from "react-router-dom";
import { UserApi } from "../api/user.ts"; import { UserApi } from "../api/user.ts";
import { useAuth } from "./hook.ts";
interface AuthContextType { interface AuthContextType {
token: string; token: string;
nickname: string;
isLoggedIn: boolean; isLoggedIn: boolean;
login: ( login: (username: string, password: string, onSuccess?: VoidFunction, onFailed?: (error: string) => void) => void;
username: string, logout: (onSuccess?: VoidFunction, onFailed?: (error: string) => void) => void;
password: string,
onSuccess?: VoidFunction,
onFailed?: (error: string) => void,
) => void;
logout: (
onSuccess?: VoidFunction,
onFailed?: (error: string) => void,
) => void;
} }
const AuthContext = React.createContext<AuthContextType>({ const AuthContext = React.createContext<AuthContextType>({
token: "", token: "",
nickname: "guest",
isLoggedIn: false, isLoggedIn: false,
login: ( login: (_username: string, _password: string, _onSuccess?: VoidFunction, onFailed?: (error: string) => void) => {
_username: string,
_password: string,
_onSuccess?: VoidFunction,
onFailed?: (error: string) => void,
) => {
onFailed && onFailed("not implemented"); onFailed && onFailed("not implemented");
}, },
logout: (_onSuccess?: VoidFunction, onFailed?: (error: string) => void) => { logout: (_onSuccess?: VoidFunction, onFailed?: (error: string) => void) => {
@ -39,7 +24,6 @@ const AuthContext = React.createContext<AuthContextType>({
function AuthProvider({ children }: { children: React.ReactNode }) { function AuthProvider({ children }: { children: React.ReactNode }) {
const [token, setToken] = useState(""); const [token, setToken] = useState("");
const [nickname, setNickname] = useState("guest");
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => { useEffect(() => {
@ -60,7 +44,6 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
.then((resp) => { .then((resp) => {
localStorage.setItem("token", resp.token); localStorage.setItem("token", resp.token);
setToken(resp.token); setToken(resp.token);
setNickname(resp.nickname);
setIsLoggedIn(true); setIsLoggedIn(true);
onSuccess && onSuccess(); onSuccess && onSuccess();
}) })
@ -71,13 +54,9 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
}); });
}; };
const logout = ( const logout = (onSuccess?: VoidFunction, onFailed?: (error: string) => void) => {
onSuccess?: VoidFunction,
onFailed?: (error: string) => void,
) => {
localStorage.removeItem("token"); localStorage.removeItem("token");
setToken(""); setToken("");
setNickname("guest");
setIsLoggedIn(false); setIsLoggedIn(false);
if (!isLoggedIn) { if (!isLoggedIn) {
@ -95,20 +74,19 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
}); });
}; };
const value = { token, nickname, isLoggedIn, login, logout }; const value = { token, isLoggedIn, login, logout };
return ( return <AuthContext.Provider value={value}> {children} </AuthContext.Provider>;
<AuthContext.Provider value={value}> {children} </AuthContext.Provider>
);
} }
function RequireAuth({ children }: { children: React.ReactNode }) { function RequireAuth({ children }: { children: React.ReactNode }) {
const auth = React.useContext(AuthContext); const auth = useAuth();
const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
if (!auth.token) { useEffect(() => {
return <Navigate to="/login" state={{ from: location }} replace />; if (!auth.isLoggedIn) navigate("/login", { state: { from: location } });
} }, [auth.isLoggedIn, location, navigate]);
return children; return children;
} }

View File

@ -18,9 +18,7 @@ interface LoginFormProps<T> {
extra?: React.ReactNode; extra?: React.ReactNode;
} }
export default function MyLoginForm<T = LoginFormValues>( export default function MyLoginForm<T = LoginFormValues>(props: LoginFormProps<T>) {
props: LoginFormProps<T>,
) {
const [msg, msgContextHolder] = message.useMessage(); const [msg, msgContextHolder] = message.useMessage();
const [errMsg, setErrMsg] = useState(""); const [errMsg, setErrMsg] = useState("");

View File

@ -20,24 +20,20 @@ export default function Markdown(props: MarkdownProps) {
const rehypePlugins = [rehypeRaw, rehypeMathJaxSvg]; const rehypePlugins = [rehypeRaw, rehypeMathJaxSvg];
const renderers: Components = { const renderers: Components = {
code: function makeCodeBlock({ code: function makeCodeBlock({ inline, className, children, ...props }) {
inline,
className,
children,
...props
}) {
const match = /language-(\w+)/.exec(className || ""); const match = /language-(\w+)/.exec(className || "");
const codeBlock = ( const codeBlock = (
<PrismAsync <PrismAsync
{...props} {...props}
language={(match || ["", "text"])[1]} language={(match || ["", "text"])[1]}
PreTag="div" PreTag="div"
children={String(children).replace(/\n$/, "")}
wrapLines={true} wrapLines={true}
showLineNumbers={true} showLineNumbers={true}
wrapLongLines={true} wrapLongLines={true}
style={oneLight} style={oneLight}
/> >
{String(children).replace(/\n$/, "")}
</PrismAsync>
); );
const codeInline = <code {...props}>{children}</code>; const codeInline = <code {...props}>{children}</code>;
return !inline && match ? codeBlock : codeInline; return !inline && match ? codeBlock : codeInline;
@ -48,11 +44,12 @@ export default function Markdown(props: MarkdownProps) {
<> <>
<ReactMarkdown <ReactMarkdown
className={"markdown-body"} className={"markdown-body"}
children={props.markdown}
remarkPlugins={remarkPlugins} remarkPlugins={remarkPlugins}
rehypePlugins={rehypePlugins} rehypePlugins={rehypePlugins}
components={renderers} components={renderers}
/> >
{props.markdown}
</ReactMarkdown>
</> </>
); );
} }

View File

@ -1,22 +1,24 @@
import React from "react"; import React from "react";
import { Link, useLoaderData } from "react-router-dom"; import { Link } from "react-router-dom";
import { Collapse, CollapseProps, Descriptions, Space, Tag } from "antd"; import { Collapse, CollapseProps, Descriptions, Space, Tag } from "antd";
import { DetailsResp } from "../api/problem.ts"; import { DetailsResp } from "../api/problem.ts";
interface ProblemDetailsProps { interface ProblemDetailsProps {
action: React.ReactNode; details: DetailsResp;
action?: React.ReactNode;
} }
export default function ProblemDetails(props: ProblemDetailsProps) { export default function ProblemDetails(props: ProblemDetailsProps) {
const details = useLoaderData() as DetailsResp; const details = props.details;
const problemInfo = ( const problemInfo = (
<Descriptions bordered column={1} size="small"> <Descriptions bordered column={1} size="small">
<Descriptions.Item label="Title">
<Link to={`/problem/${details.problem.meta.ID}`}>{details.problem.title}</Link>
</Descriptions.Item>
<Descriptions.Item label="Provider"> <Descriptions.Item label="Provider">
<Link to={`/user/${details.problem.provider.meta.ID}`}> <Link to={`/profile/${details.problem.provider.meta.ID}`}>{details.problem.provider.nick_name}</Link>
{details.problem.provider.nick_name}
</Link>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Supported Languages"> <Descriptions.Item label="Supported Languages">
<Space size={[0, 8]} wrap> <Space size={[0, 8]} wrap>
@ -25,23 +27,15 @@ export default function ProblemDetails(props: ProblemDetailsProps) {
))} ))}
</Space> </Space>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="Task Nums"> <Descriptions.Item label="Task Nums">{details.context.Tasks.length}</Descriptions.Item>
{details.context.Tasks.length}
</Descriptions.Item>
</Descriptions> </Descriptions>
); );
const runtimeLimit = ( const runtimeLimit = (
<Descriptions bordered column={1} size="small"> <Descriptions bordered column={1} size="small">
<Descriptions.Item label="Time Limit"> <Descriptions.Item label="Time Limit">{details.context.Runtime.TimeLimit} ms</Descriptions.Item>
{details.context.Runtime.TimeLimit} ms <Descriptions.Item label="Memory Limit">{details.context.Runtime.MemoryLimit} MB</Descriptions.Item>
</Descriptions.Item> <Descriptions.Item label="Process Limit">{details.context.Runtime.NProcLimit}</Descriptions.Item>
<Descriptions.Item label="Memory Limit">
{details.context.Runtime.MemoryLimit} MB
</Descriptions.Item>
<Descriptions.Item label="Process Limit">
{details.context.Runtime.NProcLimit}
</Descriptions.Item>
</Descriptions> </Descriptions>
); );
@ -56,16 +50,25 @@ export default function ProblemDetails(props: ProblemDetailsProps) {
label: "Runtime Limit", label: "Runtime Limit",
children: runtimeLimit, children: runtimeLimit,
}, },
];
const items: CollapseProps["items"] = [
...miscItems,
...(props.action
? [
{ {
key: "3", key: "3",
label: "Action", label: "Action",
children: props.action, children: props.action,
}, },
]
: []),
]; ];
const defaultActiveKey = ["1", "2", ...(props.action ? ["3"] : [])];
return ( return (
<> <>
<Collapse items={miscItems} defaultActiveKey={["1", "2", "3"]} /> <Collapse items={items} defaultActiveKey={defaultActiveKey} />
</> </>
); );
} }

View File

@ -0,0 +1,128 @@
import { Link } from "react-router-dom";
import { ProColumns, ProTable } from "@ant-design/pro-components";
import { Button, Space } from "antd";
import { StatusApi } from "../api/status.ts";
interface SubmissionInfo {
id: number;
problem_title: string;
problem_id: number;
nick_name: string;
user_id: number;
point: string;
date: Date;
}
const columns: ProColumns<SubmissionInfo>[] = [
{
title: "ID",
dataIndex: "id",
align: "center",
},
{
title: "Problem",
dataIndex: "problem_title",
align: "left",
},
{
title: "User",
dataIndex: "nick_name",
align: "left",
},
{
title: "Point",
dataIndex: "point",
align: "center",
},
{
title: "Date",
dataIndex: "date",
valueType: "dateTime",
align: "left",
},
{
title: "Action",
dataIndex: "action",
render: (_node, entity) => {
return (
<Space>
<Button>
<Link to={`/problem/${entity.problem_id}`}></Link>
</Button>
<Button>
<Link to={`/profile/${entity.user_id}`}></Link>
</Button>
<Button>
<Link to={`/details/${entity.id}`}></Link>
</Button>
</Space>
);
},
},
];
interface StatusTableProps {
title?: string;
pid?: number;
uid?: number;
token: string;
}
export default function StatusTable(props: StatusTableProps) {
const request = async (
params: Record<string, string> & {
pageSize?: number;
current?: number;
keyword?: string | undefined;
},
) => {
try {
const cur = params.current || 1;
const size = params.pageSize || 10;
const res = await StatusApi.Query(
{
pid: props.pid,
uid: props.uid,
offset: (cur - 1) * (size || 10),
limit: size,
},
props.token,
);
const data = res.data.map((i) => {
return {
id: i.submission.meta.ID,
problem_title: i.submission.problem.title,
problem_id: i.submission.problem.meta.ID,
nick_name: i.submission.user.nick_name,
user_id: i.submission.user.meta.ID,
point: i.point === -1 ? "Pending" : i.point.toString(),
date: i.submission.meta.CreatedAt,
};
});
return { success: true, data: data, total: res.count };
} catch (e) {
console.log(e);
return { success: false, data: [], total: 0 };
}
};
return (
<ProTable<SubmissionInfo>
cardBordered
columns={columns}
request={request}
rowKey={(record) => record.id}
pagination={{
pageSize: 10,
}}
search={false}
dateFormatter="string"
headerTitle={props.title || "Judge Status"}
/>
);
}

41
src/components/user.tsx Normal file
View File

@ -0,0 +1,41 @@
import { useEffect, useState } from "react";
import dayjs from "dayjs";
import { Collapse, CollapseProps, Descriptions } from "antd";
import { UserApi, UserProfile, UserRole } from "../api/user.ts";
interface UserInfoProps {
uid: number;
token: string;
}
export default function UserInfo(props: UserInfoProps) {
const [profile, setProfile] = useState<UserProfile>();
useEffect(() => {
void UserApi.Profile({ uid: props.uid }, props.token).then(setProfile);
}, [props.uid, props.token]);
const roleName = Object.entries(UserRole).filter(([, v]) => v === profile?.role);
const items: CollapseProps["items"] = [
{
key: "1",
label: "User Info",
children: (
<Descriptions bordered column={1} size="small">
<Descriptions.Item label="Nickname">{profile?.nick_name || "Not Found"}</Descriptions.Item>
<Descriptions.Item label="Joined at">
{(profile?.meta.CreatedAt && dayjs(profile?.meta.CreatedAt).format("YYYY-MM-DD HH:mm:ss")) ||
"N/A"}
</Descriptions.Item>
<Descriptions.Item label="Role">
{(roleName.length > 0 && roleName[0][0]) || "N/A"}
</Descriptions.Item>
</Descriptions>
),
},
];
return <Collapse items={items} defaultActiveKey={["1"]} />;
}

View File

@ -1,7 +1,6 @@
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Droid Sans", "Helvetica Neue", sans-serif;
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@ -9,6 +8,5 @@ body {
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
monospace;
} }

View File

@ -43,8 +43,7 @@ Sentry.init({
replaysOnErrorSampleRate: 1.0, replaysOnErrorSampleRate: 1.0,
}); });
const sentryCreateBrowserRouter = const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouter(createBrowserRouter);
Sentry.wrapCreateBrowserRouter(createBrowserRouter);
const router = sentryCreateBrowserRouter([ const router = sentryCreateBrowserRouter([
{ {

124
src/pages/details.tsx Normal file
View File

@ -0,0 +1,124 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { Card, Col, Row, Tabs, TabsProps, Tooltip } from "antd";
import { PrismAsync } from "react-syntax-highlighter";
import { oneLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
import ProblemDetails from "../components/problem-details.tsx";
import { useAuth } from "../components/hook.ts";
import { DetailsResp } from "../api/problem.ts";
import { ProblemLoader } from "../api/loader.ts";
import { StatusApi, StatusInfo, Verdict } from "../api/status.ts";
const VerdictMap: {
[key in Verdict]: { color: string; tip: string; label: string; name: string };
} = {
[Verdict.Accepted]: { color: "#52C41A", tip: "#72E43A", label: "AC", name: "Accepted" },
[Verdict.WrongAnswer]: { color: "#E74C3C", tip: "#FF6C5C", label: "WA", name: "Wrong Answer" },
[Verdict.JuryFailed]: { color: "#0E1D69", tip: "#2E3D89", label: "JF", name: "Jury Failed" },
[Verdict.PartialCorrect]: { color: "#E67E22", tip: "#FF9E42", label: "PC", name: "Partial Correct" },
[Verdict.TimeLimitExceeded]: { color: "#052242", tip: "#254262", label: "TLE", name: "Time Limit Exceeded" },
[Verdict.MemoryLimitExceeded]: { color: "#052242", tip: "#254262", label: "MLE", name: "Memory Limit Exceeded" },
[Verdict.RuntimeError]: { color: "#9D3DCF", tip: "#BD5DEF", label: "RE", name: "Runtime Error" },
[Verdict.CompileError]: { color: "#FADB14", tip: "#FFFB34", label: "CE", name: "Compile Error" },
[Verdict.SystemError]: { color: "#0E1D69", tip: "#2E3D89", label: "UKE", name: "System Error" },
};
const gridStyle: React.CSSProperties = {
width: "5.5em",
height: "5.5em",
margin: "0.2em",
fontSize: "1.5em",
padding: "0.5em",
textAlign: "center",
color: "white",
};
const topLeftStyle: React.CSSProperties = {
position: "absolute",
fontSize: "0.7em",
};
const labelStyle: React.CSSProperties = {
fontSize: "1.2em",
paddingTop: "0.6em",
textAlign: "center",
};
const statusStyle: React.CSSProperties = {
fontSize: "0.65em",
textAlign: "center",
};
export default function DetailsPage() {
const auth = useAuth();
const { id } = useParams();
const id_num = +(id || "0");
const [status, setStatus] = useState<StatusInfo>();
const [details, setDetails] = useState<DetailsResp>();
useEffect(() => {
void StatusApi.QueryBySubmission({ sid: id_num }, auth.token).then(setStatus);
}, [id_num, auth.token]);
useEffect(() => {
if (!status) return;
void ProblemLoader({
params: { id: status.submission.problem.meta.ID.toString() },
}).then(setDetails);
}, [status]);
const taskInfo = (
<Card style={{ padding: "1em" }}>
{status?.context?.tasks?.map((task) => (
<Card.Grid key={task.id} style={{ ...gridStyle, background: VerdictMap[task.verdict].color }}>
<Tooltip title={task.message} color={VerdictMap[task.verdict].tip}>
<div style={topLeftStyle}>{`#${task.id}`}</div>
<div>
<div style={labelStyle}>{VerdictMap[task.verdict].label}</div>
<div style={statusStyle}>{`${task.real_time}ms/${task.memory}KB`}</div>
</div>
</Tooltip>
</Card.Grid>
))}
</Card>
);
const sourceCode = (
<PrismAsync
language={status?.submission.code === "" ? "text" : status?.submission.language || "text"}
wrapLines={true}
showLineNumbers={true}
wrapLongLines={true}
style={oneLight}
>
{status?.submission.code || "Not Available"}
</PrismAsync>
);
const items: TabsProps["items"] = [
{
key: "1",
label: "Task Info",
children: taskInfo,
},
{
key: "2",
label: "Source Code",
children: sourceCode,
},
];
const body = <Tabs defaultActiveKey="1" items={items} />;
return (
<>
<Row justify="center" align="top" gutter={[16, 16]}>
<Col span={18}>{body}</Col>
<Col span={6}>{details && <ProblemDetails details={details} />}</Col>
</Row>
</>
);
}

View File

@ -1,8 +1,4 @@
import { import { useRouteError, isRouteErrorResponse, useNavigate } from "react-router-dom";
useRouteError,
isRouteErrorResponse,
useNavigate,
} from "react-router-dom";
import { Button, Result, Space } from "antd"; import { Button, Result, Space } from "antd";
const ErrorPage = () => { const ErrorPage = () => {

View File

@ -1,3 +1,4 @@
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAuth } from "../components/hook.ts"; import { useAuth } from "../components/hook.ts";
@ -7,6 +8,22 @@ export default function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const auth = useAuth(); const auth = useAuth();
const isFirstRender = useRef(true);
useEffect(
() => {
if (isFirstRender.current) {
if (auth.isLoggedIn) {
if (window.history?.length) navigate(-1);
else navigate("/");
}
}
isFirstRender.current = false;
return () => void (isFirstRender.current = true);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const onFinish: CallbackType = (values, setErrMsg, msg) => { const onFinish: CallbackType = (values, setErrMsg, msg) => {
auth.login( auth.login(
values.username, values.username,

View File

@ -14,25 +14,18 @@ export default function ProblemPage() {
<Space wrap> <Space wrap>
<Button <Button
icon={<PlayCircleOutlined />} icon={<PlayCircleOutlined />}
onClick={() => onClick={() => navigate(`/problem/${details.problem.meta.ID}/submit`)}
navigate(`/problem/${details.problem.meta.ID}/submit`)
}
> >
Submit Submit
</Button> </Button>
<Button <Button icon={<SearchOutlined />} onClick={() => navigate(`/problem/${details.problem.meta.ID}/status`)}>
icon={<SearchOutlined />}
onClick={() =>
navigate(`/problem/${details.problem.meta.ID}/status`)
}
>
Status Status
</Button> </Button>
</Space> </Space>
); );
const ProblemStatement = <Markdown markdown={details.problem.statement} />; const ProblemStatement = <Markdown markdown={details.problem.statement} />;
const MiscPanel = <ProblemDetails action={actionBtn} />; const MiscPanel = <ProblemDetails details={details} action={actionBtn} />;
return ( return (
<> <>

25
src/pages/profile.tsx Normal file
View File

@ -0,0 +1,25 @@
import { useParams } from "react-router-dom";
import { Col, Row } from "antd";
import { useAuth } from "../components/hook.ts";
import StatusTable from "../components/status-table.tsx";
import UserInfo from "../components/user.tsx";
export default function ProfilePage() {
const auth = useAuth();
const { id } = useParams();
const id_num = +(id || "0");
return (
<>
<Row justify="center" align="top" gutter={[16, 16]}>
<Col span={18}>
<StatusTable uid={id_num} token={auth.token} />
</Col>
<Col span={6}>
<UserInfo uid={id_num} token={auth.token} />
</Col>
</Row>
</>
);
}

View File

@ -15,20 +15,14 @@ type RegisterFormValues = {
export default function RegisterPage() { export default function RegisterPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const onFinish: CallbackType<RegisterFormValues> = ( const onFinish: CallbackType<RegisterFormValues> = (values, setErrMsg, msg) => {
values,
setErrMsg,
msg,
) => {
UserApi.Create({ UserApi.Create({
username: values.username, username: values.username,
nickname: values.nickname, nickname: values.nickname,
password: values.password, password: values.password,
}) })
.then(() => { .then(() => {
void msg void msg.success("注册成功").then(() => navigate("/login", { replace: true }));
.success("注册成功")
.then(() => navigate("/login", { replace: true }));
}) })
.catch((err: string) => setErrMsg(err)); .catch((err: string) => setErrMsg(err));
}; };

View File

@ -1,19 +1,8 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { import { DefaultFooter, PageContainer, ProLayout, ProLayoutProps } from "@ant-design/pro-components";
DefaultFooter, import { BlockOutlined, LoginOutlined, LogoutOutlined, PlusCircleOutlined, ProfileOutlined } from "@ant-design/icons";
PageContainer,
ProLayout,
ProLayoutProps,
} from "@ant-design/pro-components";
import {
BlockOutlined,
LoginOutlined,
LogoutOutlined,
PlusCircleOutlined,
ProfileOutlined,
} from "@ant-design/icons";
import { Dropdown, message, Skeleton } from "antd"; import { Dropdown, message, Skeleton } from "antd";
import { NavConfigs } from "../routes.tsx"; import { NavConfigs } from "../routes.tsx";
@ -36,9 +25,7 @@ const LayoutProps: ProLayoutProps = {
}), }),
}, },
menuItemRender: (item, dom) => <Link to={item.path || "/"}>{dom}</Link>, menuItemRender: (item, dom) => <Link to={item.path || "/"}>{dom}</Link>,
footerRender: () => ( footerRender: () => <DefaultFooter copyright="2023 WOJ Created by WHUPRJ" />,
<DefaultFooter copyright="2023 WOJ Created by WHUPRJ" />
),
}; };
const AvatarUserItems = [ const AvatarUserItems = [
@ -115,9 +102,7 @@ export default function Root() {
return ( return (
<Dropdown <Dropdown
menu={{ menu={{
items: auth.isLoggedIn items: auth.isLoggedIn ? AvatarUserItems : AvatarGuestItems,
? AvatarUserItems
: AvatarGuestItems,
onClick: (key) => avatarActions[key.key](), onClick: (key) => avatarActions[key.key](),
}} }}
> >
@ -128,11 +113,7 @@ export default function Root() {
}; };
return ( return (
<ProLayout <ProLayout {...LayoutProps} location={{ pathname: curTab }} avatarProps={avatarProps}>
{...LayoutProps}
location={{ pathname: curTab }}
avatarProps={avatarProps}
>
{msgContextHolder} {msgContextHolder}
<PageContainer> <PageContainer>
<React.Suspense fallback={<Skeleton />}> <React.Suspense fallback={<Skeleton />}>

View File

@ -1,7 +1,9 @@
import { Link } from "react-router-dom";
import { ProColumns, ProTable } from "@ant-design/pro-components"; import { ProColumns, ProTable } from "@ant-design/pro-components";
import { Button } from "antd";
import { ProblemApi } from "../api/problem.ts"; import { ProblemApi } from "../api/problem.ts";
import { Link } from "react-router-dom";
interface ProblemList { interface ProblemList {
id: number; id: number;
@ -32,7 +34,11 @@ const columns: ProColumns<ProblemList>[] = [
hideInSearch: true, hideInSearch: true,
dataIndex: "action", dataIndex: "action",
render: (_node, entity) => { render: (_node, entity) => {
return <Link to={`/problem/${entity.id}`}>View</Link>; return (
<Button>
<Link to={`/problem/${entity.id}`}>View</Link>
</Button>
);
}, },
}, },
]; ];

26
src/pages/status.tsx Normal file
View File

@ -0,0 +1,26 @@
import StatusTable from "../components/status-table.tsx";
import { useLoaderData, useParams } from "react-router-dom";
import { useAuth } from "../components/hook.ts";
import { Col, Row } from "antd";
import ProblemDetails from "../components/problem-details.tsx";
import { DetailsResp } from "../api/problem.ts";
export default function ProblemStatusPage() {
const auth = useAuth();
const details = useLoaderData() as DetailsResp;
const { id } = useParams();
return (
<>
<Row justify="center" align="top" gutter={[16, 16]}>
<Col span={18}>
{" "}
<StatusTable pid={+(id || "0")} token={auth.token} />
</Col>
<Col span={6}>
<ProblemDetails details={details} />
</Col>
</Row>
</>
);
}

View File

@ -27,9 +27,7 @@ export default function SubmitPage() {
const auth = useAuth(); const auth = useAuth();
const [msg, msgContextHolder] = message.useMessage(); const [msg, msgContextHolder] = message.useMessage();
const langOptions = AvailLang.filter((l) => const langOptions = AvailLang.filter((l) => details.context.Languages.some((x) => x.Lang === l.value));
details.context.Languages.some((x) => x.Lang === l.value),
);
const [lang, setLang] = useState(langOptions[0].value); const [lang, setLang] = useState(langOptions[0].value);
const [code, setCode] = useState(""); const [code, setCode] = useState("");
@ -70,7 +68,7 @@ export default function SubmitPage() {
onChange: setCode, onChange: setCode,
}); });
const MiscPanel = <ProblemDetails action={funcMenu} />; const MiscPanel = <ProblemDetails action={funcMenu} details={details} />;
return ( return (
<> <>

View File

@ -2,18 +2,17 @@ import React from "react";
import { ProblemLoader } from "./api/loader.ts"; import { ProblemLoader } from "./api/loader.ts";
import { RequireAuth } from "./components/auth.tsx"; import { RequireAuth } from "./components/auth.tsx";
import { import { HomeOutlined, ProfileOutlined, QuestionCircleOutlined } from "@ant-design/icons";
HomeOutlined,
ProfileOutlined,
QuestionCircleOutlined,
} from "@ant-design/icons";
const DetailsPage = React.lazy(() => import("./pages/details.tsx"));
const HomePage = React.lazy(() => import("./pages/home.tsx")); const HomePage = React.lazy(() => import("./pages/home.tsx"));
const LoginPage = React.lazy(() => import("./pages/login.tsx")); const LoginPage = React.lazy(() => import("./pages/login.tsx"));
const LogoutPage = React.lazy(() => import("./pages/logout.tsx")); const LogoutPage = React.lazy(() => import("./pages/logout.tsx"));
const ProblemPage = React.lazy(() => import("./pages/problem.tsx"));
const ProblemStatusPage = React.lazy(() => import("./pages/status.tsx"));
const ProfilePage = React.lazy(() => import("./pages/profile.tsx"));
const RegisterPage = React.lazy(() => import("./pages/register.tsx")); const RegisterPage = React.lazy(() => import("./pages/register.tsx"));
const SearchPage = React.lazy(() => import("./pages/search.tsx")); const SearchPage = React.lazy(() => import("./pages/search.tsx"));
const ProblemPage = React.lazy(() => import("./pages/problem.tsx"));
const SubmitPage = React.lazy(() => import("./pages/submit.tsx")); const SubmitPage = React.lazy(() => import("./pages/submit.tsx"));
const RouteConfigs = [ const RouteConfigs = [
@ -37,14 +36,6 @@ const RouteConfigs = [
path: "register", path: "register",
element: <RegisterPage />, element: <RegisterPage />,
}, },
{
path: "profile",
element: (
<RequireAuth>
<p>Profile</p>
</RequireAuth>
),
},
{ {
path: "search", path: "search",
element: <SearchPage />, element: <SearchPage />,
@ -63,6 +54,39 @@ const RouteConfigs = [
), ),
loader: ProblemLoader, loader: ProblemLoader,
}, },
{
path: "problem/:id/status",
element: (
<RequireAuth>
<ProblemStatusPage />
</RequireAuth>
),
loader: ProblemLoader,
},
{
path: "profile",
element: (
<RequireAuth>
<ProfilePage />
</RequireAuth>
),
},
{
path: "profile/:id",
element: (
<RequireAuth>
<ProfilePage />
</RequireAuth>
),
},
{
path: "details/:id",
element: (
<RequireAuth>
<DetailsPage />
</RequireAuth>
),
},
]; ];
const NavConfigs = [ const NavConfigs = [