Compare commits
8 Commits
a723b02f19
...
766074c6c3
Author | SHA1 | Date | |
---|---|---|---|
766074c6c3 | |||
d69f6a5b86 | |||
2474012a1a | |||
0fb4bde795 | |||
d380daa365 | |||
ec1bdf6c03 | |||
4ee1b98699 | |||
2dfdec85ed |
44
package.json
44
package.json
@ -10,38 +10,40 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^5.1.4",
|
"@ant-design/icons": "^5.2.6",
|
||||||
"@ant-design/pro-components": "^2.6.7",
|
"@ant-design/pro-components": "^2.6.43",
|
||||||
"@sentry/react": "^7.58.1",
|
"@sentry/react": "^7.90.0",
|
||||||
"@sentry/vite-plugin": "^2.4.0",
|
"@sentry/vite-plugin": "^2.10.2",
|
||||||
"antd": "^5.7.0",
|
"ace-builds": "^1.32.2",
|
||||||
"axios": "^1.4.0",
|
"antd": "^5.12.4",
|
||||||
"github-markdown-css": "^5.2.0",
|
"axios": "^1.6.2",
|
||||||
|
"github-markdown-css": "^5.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.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.14.1",
|
"react-router-dom": "^6.21.0",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"rehype-mathjax": "^4.0.2",
|
"rehype-mathjax": "^4.0.3",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"remark-emoji": "^3.1.2",
|
"remark-emoji": "^3.1.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1"
|
"remark-math": "^5.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.4.2",
|
"@types/node": "^20.10.5",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.45",
|
||||||
"@types/react-dom": "^18.2.6",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-syntax-highlighter": "^15.5.7",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"eslint": "^8.44.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.1",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"prettier": "3.0.0",
|
"prettier": "3.0.0",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^4.4.0"
|
"vite": "^4.5.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2132
pnpm-lock.yaml
2132
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,3 +0,0 @@
|
|||||||
export { send } from "./base";
|
|
||||||
|
|
||||||
export { UserApi } from "./user.ts";
|
|
@ -25,7 +25,7 @@ export async function send<D, T>(
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const resp = await axios.post<ResponseWrap<T>>(api, data, {
|
const resp = await axios.post<ResponseWrap<T>>(api, data, {
|
||||||
headers: { token: token },
|
headers: { Authorization: token },
|
||||||
});
|
});
|
||||||
if (resp.data.code !== 0) return Promise.reject(resp.data.msg);
|
if (resp.data.code !== 0) return Promise.reject(resp.data.msg);
|
||||||
return resp.data.body;
|
return resp.data.body;
|
||||||
|
9
src/api/loader.ts
Normal file
9
src/api/loader.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ProblemApi } from "./problem.ts";
|
||||||
|
|
||||||
|
export async function ProblemLoader({ params }: { params: { id: string } }) {
|
||||||
|
const id = parseInt(params.id);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
throw new Error("invalid problem id");
|
||||||
|
}
|
||||||
|
return await ProblemApi.Details({ pid: id });
|
||||||
|
}
|
@ -7,6 +7,11 @@ export interface UserReq {
|
|||||||
uid?: number;
|
uid?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserLoginResp {
|
||||||
|
nickname: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
meta: Meta;
|
meta: Meta;
|
||||||
user_name: string;
|
user_name: string;
|
||||||
@ -23,7 +28,7 @@ export class UserApi {
|
|||||||
return send("/user/create", data);
|
return send("/user/create", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Login(data: UserReq): Promise<string> {
|
static async Login(data: UserReq): Promise<UserLoginResp> {
|
||||||
if (!data.username || !data.password) {
|
if (!data.username || !data.password) {
|
||||||
throw new Error("Missing required fields");
|
throw new Error("Missing required fields");
|
||||||
}
|
}
|
||||||
|
116
src/components/auth.tsx
Normal file
116
src/components/auth.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { UserApi } from "../api/user.ts";
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
token: string;
|
||||||
|
nickname: string;
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
login: (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
onSuccess: VoidFunction,
|
||||||
|
onFailed: (error: string) => void,
|
||||||
|
) => void;
|
||||||
|
logout: (
|
||||||
|
onSuccess: VoidFunction,
|
||||||
|
onFailed: (error: string) => void,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = React.createContext<AuthContextType>({
|
||||||
|
token: "",
|
||||||
|
nickname: "guest",
|
||||||
|
isLoggedIn: false,
|
||||||
|
login: (
|
||||||
|
_username: string,
|
||||||
|
_password: string,
|
||||||
|
_onSuccess: VoidFunction,
|
||||||
|
onFailed: (error: string) => void,
|
||||||
|
) => {
|
||||||
|
onFailed("not implemented");
|
||||||
|
},
|
||||||
|
logout: (_onSuccess: VoidFunction, onFailed: (error: string) => void) => {
|
||||||
|
onFailed("not implemented");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [token, setToken] = useState("");
|
||||||
|
const [nickname, setNickname] = useState("guest");
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
setToken(token);
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = (
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
onSuccess: VoidFunction,
|
||||||
|
onFailed: (error: string) => void,
|
||||||
|
) => {
|
||||||
|
UserApi.Login({ username: username, password: password })
|
||||||
|
.then((resp) => {
|
||||||
|
localStorage.setItem("token", resp.token);
|
||||||
|
setToken(resp.token);
|
||||||
|
setNickname(resp.nickname);
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
onSuccess();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("[user] userLogin", err);
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
onFailed(err as string);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = (
|
||||||
|
onSuccess: VoidFunction,
|
||||||
|
onFailed: (error: string) => void,
|
||||||
|
) => {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
setToken("");
|
||||||
|
setNickname("guest");
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
onFailed("not logged in");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserApi.Logout(token)
|
||||||
|
.then(() => {
|
||||||
|
onSuccess();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("[user] userLogout", err);
|
||||||
|
onFailed(err as string);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = { token, nickname, isLoggedIn, login, logout };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={value}> {children} </AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RequireAuth({ children }: { children: React.ReactNode }) {
|
||||||
|
const auth = React.useContext(AuthContext);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (!auth.token) {
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AuthProvider, RequireAuth, AuthContext };
|
40
src/components/editor.tsx
Normal file
40
src/components/editor.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import AceEditor from "react-ace";
|
||||||
|
|
||||||
|
import "ace-builds/src-noconflict/mode-c_cpp";
|
||||||
|
import "ace-builds/src-noconflict/mode-python";
|
||||||
|
import "ace-builds/src-noconflict/snippets/c_cpp";
|
||||||
|
import "ace-builds/src-noconflict/snippets/python";
|
||||||
|
|
||||||
|
import "ace-builds/src-noconflict/theme-github";
|
||||||
|
import "ace-builds/src-noconflict/ext-language_tools";
|
||||||
|
|
||||||
|
interface EditorProps {
|
||||||
|
mode: string;
|
||||||
|
onChange: (code: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Editor(props: EditorProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AceEditor
|
||||||
|
name="code_editor"
|
||||||
|
fontSize={16}
|
||||||
|
height="75vh"
|
||||||
|
width="100%"
|
||||||
|
mode={props.mode}
|
||||||
|
theme="github"
|
||||||
|
onChange={(code: string) => props.onChange(code)} // force converts to string
|
||||||
|
editorProps={{
|
||||||
|
$blockScrolling: false,
|
||||||
|
$enableMultiselect: true,
|
||||||
|
}}
|
||||||
|
setOptions={{
|
||||||
|
enableBasicAutocompletion: true,
|
||||||
|
enableLiveAutocompletion: true,
|
||||||
|
enableSnippets: true,
|
||||||
|
wrap: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
6
src/components/hook.ts
Normal file
6
src/components/hook.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { AuthContext } from "./auth.tsx";
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
return React.useContext(AuthContext);
|
||||||
|
}
|
71
src/components/problem-details.tsx
Normal file
71
src/components/problem-details.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Link, useLoaderData } from "react-router-dom";
|
||||||
|
import { Collapse, CollapseProps, Descriptions, Space, Tag } from "antd";
|
||||||
|
|
||||||
|
import { DetailsResp } from "../api/problem.ts";
|
||||||
|
|
||||||
|
interface ProblemDetailsProps {
|
||||||
|
action: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProblemDetails(props: ProblemDetailsProps) {
|
||||||
|
const details = useLoaderData() as DetailsResp;
|
||||||
|
|
||||||
|
const problemInfo = (
|
||||||
|
<Descriptions bordered column={1} size="small">
|
||||||
|
<Descriptions.Item label="Provider">
|
||||||
|
<Link to={`/user/${details.problem.provider.meta.ID}`}>
|
||||||
|
{details.problem.provider.nick_name}
|
||||||
|
</Link>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Supported Languages">
|
||||||
|
<Space size={[0, 8]} wrap>
|
||||||
|
{details.context.Languages.map((l) => (
|
||||||
|
<Tag key={l.Lang}>{l.Lang}</Tag>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="Task Nums">
|
||||||
|
{details.context.Tasks.length}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
);
|
||||||
|
|
||||||
|
const runtimeLimit = (
|
||||||
|
<Descriptions bordered column={1} size="small">
|
||||||
|
<Descriptions.Item label="Time Limit">
|
||||||
|
{details.context.Runtime.TimeLimit} ms
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
|
||||||
|
const miscItems: CollapseProps["items"] = [
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: "Problem Info",
|
||||||
|
children: problemInfo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2",
|
||||||
|
label: "Runtime Limit",
|
||||||
|
children: runtimeLimit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "3",
|
||||||
|
label: "Action",
|
||||||
|
children: props.action,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Collapse items={miscItems} defaultActiveKey={["1", "2", "3"]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -10,13 +10,15 @@ import {
|
|||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
|
import { AuthProvider } from "./components/auth.tsx";
|
||||||
import { Root, HomePage, ErrorPage } from "./pages/pages.tsx";
|
import { Root, HomePage, ErrorPage } from "./pages/pages.tsx";
|
||||||
import { RouteConfigs } from "./routes.tsx";
|
import { RouteConfigs } from "./routes.tsx";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: "https://4f90ea95bb8b462c8d8432ddbabac9b8@o354675.ingest.sentry.io/4505537167491072",
|
dsn: "https://903205e5bdddd0bb8f7625a63b161764@o4506423190355968.ingest.sentry.io/4506428723757056",
|
||||||
|
environment: import.meta.env.MODE,
|
||||||
integrations: [
|
integrations: [
|
||||||
new Sentry.BrowserTracing({
|
new Sentry.BrowserTracing({
|
||||||
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||||
@ -70,6 +72,8 @@ const router = sentryCreateBrowserRouter([
|
|||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<RouterProvider router={router} />
|
<AuthProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</AuthProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
@ -11,7 +11,7 @@ const ErrorPage = () => {
|
|||||||
|
|
||||||
const convertError = (error: unknown): string => {
|
const convertError = (error: unknown): string => {
|
||||||
if (isRouteErrorResponse(error)) {
|
if (isRouteErrorResponse(error)) {
|
||||||
return error.error?.message || error.statusText;
|
return `${error.status} ${error.statusText}`;
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
return error.message;
|
return error.message;
|
||||||
} else if (typeof error === "string") {
|
} else if (typeof error === "string") {
|
||||||
|
86
src/pages/login.tsx
Normal file
86
src/pages/login.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { message } from "antd";
|
||||||
|
import { LoginForm, ProFormText } from "@ant-design/pro-components";
|
||||||
|
import { LockOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
import { useAuth } from "../components/hook.ts";
|
||||||
|
|
||||||
|
export function LoginPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const auth = useAuth();
|
||||||
|
const [msg, msgContextHolder] = message.useMessage();
|
||||||
|
const [errMsg, setErrMsg] = useState("");
|
||||||
|
|
||||||
|
const onSuccess = async () => {
|
||||||
|
await msg.open({
|
||||||
|
type: "success",
|
||||||
|
content: "登录成功",
|
||||||
|
duration: 1,
|
||||||
|
});
|
||||||
|
if (window.history?.length) navigate(-1);
|
||||||
|
else navigate("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
async function onFinish(values: { username: string; password: string }) {
|
||||||
|
const username = values.username;
|
||||||
|
const password = values.password;
|
||||||
|
|
||||||
|
auth.login(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
() => {
|
||||||
|
void onSuccess();
|
||||||
|
},
|
||||||
|
(err) => setErrMsg(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (errMsg) {
|
||||||
|
void msg.error(errMsg).then(() => setErrMsg(""));
|
||||||
|
}
|
||||||
|
}, [errMsg, msg]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{msgContextHolder}
|
||||||
|
<LoginForm
|
||||||
|
title="WOJ"
|
||||||
|
subTitle="WHU Online Judge"
|
||||||
|
onFinish={onFinish}
|
||||||
|
>
|
||||||
|
<ProFormText
|
||||||
|
name="username"
|
||||||
|
fieldProps={{
|
||||||
|
size: "large",
|
||||||
|
prefix: <UserOutlined className={"prefixIcon"} />,
|
||||||
|
}}
|
||||||
|
placeholder={"用户名"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入用户名!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ProFormText.Password
|
||||||
|
name="password"
|
||||||
|
fieldProps={{
|
||||||
|
size: "large",
|
||||||
|
prefix: <LockOutlined className={"prefixIcon"} />,
|
||||||
|
}}
|
||||||
|
placeholder={"密码"}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入密码!",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</LoginForm>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
27
src/pages/logout.tsx
Normal file
27
src/pages/logout.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Result } from "antd";
|
||||||
|
import { SmileOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
import { useAuth } from "../components/hook.ts";
|
||||||
|
|
||||||
|
export function LogoutPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const auth = useAuth();
|
||||||
|
|
||||||
|
const goHome = () => setTimeout(() => navigate("/"), 1500);
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
auth.logout(goHome, goHome);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Result icon={<SmileOutlined />} title={"See you next time!"} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -2,5 +2,9 @@ export { Root } from "./root";
|
|||||||
export { ErrorPage } from "./error-page";
|
export { ErrorPage } from "./error-page";
|
||||||
|
|
||||||
export { HomePage } from "./home";
|
export { HomePage } from "./home";
|
||||||
export { ProblemLoader, ProblemPage } from "./problem";
|
export { ProblemPage } from "./problem";
|
||||||
export { SearchPage } from "./search";
|
export { SearchPage } from "./search";
|
||||||
|
export { SubmitPage } from "./submit";
|
||||||
|
|
||||||
|
export { LoginPage } from "./login.tsx";
|
||||||
|
export { LogoutPage } from "./logout.tsx";
|
||||||
|
@ -1,63 +1,15 @@
|
|||||||
import {
|
import { Row, Col, Space, Button } from "antd";
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Collapse,
|
|
||||||
CollapseProps,
|
|
||||||
Descriptions,
|
|
||||||
Tag,
|
|
||||||
Space,
|
|
||||||
Button,
|
|
||||||
} from "antd";
|
|
||||||
import { PlayCircleOutlined, SearchOutlined } from "@ant-design/icons";
|
import { PlayCircleOutlined, SearchOutlined } from "@ant-design/icons";
|
||||||
import { Link, useLoaderData, useNavigate } from "react-router-dom";
|
import { useLoaderData, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import Markdown from "../components/markdown.tsx";
|
import Markdown from "../components/markdown.tsx";
|
||||||
import { DetailsResp, ProblemApi } from "../api/problem.ts";
|
import { DetailsResp } from "../api/problem.ts";
|
||||||
|
import ProblemDetails from "../components/problem-details.tsx";
|
||||||
export async function ProblemLoader({ params }: { params: { id: string } }) {
|
|
||||||
const id = parseInt(params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
throw new Error("invalid problem id");
|
|
||||||
}
|
|
||||||
return await ProblemApi.Details({ pid: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProblemPage() {
|
export function ProblemPage() {
|
||||||
const details = useLoaderData() as DetailsResp;
|
const details = useLoaderData() as DetailsResp;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const problemInfo = (
|
|
||||||
<Descriptions bordered column={1} size="small">
|
|
||||||
<Descriptions.Item label="Provider">
|
|
||||||
<Link to={`/user/${details.problem.provider.meta.ID}`}>
|
|
||||||
{details.problem.provider.nick_name}
|
|
||||||
</Link>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Supported Languages">
|
|
||||||
<Space size={[0, 8]} wrap>
|
|
||||||
{details.context.Languages.map((l) => (
|
|
||||||
<Tag key={l.Lang}>{l.Lang}</Tag>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Task Nums">
|
|
||||||
{details.context.Tasks.length}
|
|
||||||
</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
);
|
|
||||||
const runtimeLimit = (
|
|
||||||
<Descriptions bordered column={1} size="small">
|
|
||||||
<Descriptions.Item label="Time Limit">
|
|
||||||
{details.context.Runtime.TimeLimit} ms
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
const actionBtn = (
|
const actionBtn = (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
@ -79,28 +31,8 @@ export function ProblemPage() {
|
|||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
||||||
const miscItems: CollapseProps["items"] = [
|
|
||||||
{
|
|
||||||
key: "1",
|
|
||||||
label: "Problem Info",
|
|
||||||
children: problemInfo,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "2",
|
|
||||||
label: "Runtime Limit",
|
|
||||||
children: runtimeLimit,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "3",
|
|
||||||
label: "Action",
|
|
||||||
children: actionBtn,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ProblemStatement = <Markdown markdown={details.problem.statement} />;
|
const ProblemStatement = <Markdown markdown={details.problem.statement} />;
|
||||||
const MiscPanel = (
|
const MiscPanel = <ProblemDetails action={actionBtn} />;
|
||||||
<Collapse items={miscItems} defaultActiveKey={["1", "2", "3"]} />
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
84
src/pages/submit.tsx
Normal file
84
src/pages/submit.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useLoaderData } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Button, Col, message, Row, Select, Space } from "antd";
|
||||||
|
import { PlayCircleOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
import Editor from "../components/editor.tsx";
|
||||||
|
import ProblemDetails from "../components/problem-details.tsx";
|
||||||
|
import { useAuth } from "../components/hook.ts";
|
||||||
|
import { DetailsResp } from "../api/problem.ts";
|
||||||
|
import { SubmissionApi } from "../api/submission.ts";
|
||||||
|
|
||||||
|
const AvailLang = [
|
||||||
|
{ value: "cpp", label: "C++" },
|
||||||
|
{ value: "c", label: "C" },
|
||||||
|
{ value: "python", label: "Python" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LangToMode: { [lang: string]: string } = {
|
||||||
|
cpp: "c_cpp",
|
||||||
|
c: "c_cpp",
|
||||||
|
python: "python",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SubmitPage() {
|
||||||
|
const details = useLoaderData() as DetailsResp;
|
||||||
|
const auth = useAuth();
|
||||||
|
const [msg, msgContextHolder] = message.useMessage();
|
||||||
|
|
||||||
|
const langOptions = AvailLang.filter((l) =>
|
||||||
|
details.context.Languages.some((x) => x.Lang === l.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [lang, setLang] = useState(langOptions[0].value);
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
|
||||||
|
const submitCode = () => {
|
||||||
|
console.debug(lang, code);
|
||||||
|
SubmissionApi.Create(
|
||||||
|
{
|
||||||
|
pid: details.problem.meta.ID,
|
||||||
|
language: lang,
|
||||||
|
code: code,
|
||||||
|
},
|
||||||
|
auth.token,
|
||||||
|
).then(
|
||||||
|
// TODO: onSuccess: jump to status page
|
||||||
|
() => msg.success("Submit success"),
|
||||||
|
(err: string) => msg.error("Failed to submit: " + err),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const funcMenu = (
|
||||||
|
<Space wrap>
|
||||||
|
<Select
|
||||||
|
placeholder="Select a person"
|
||||||
|
optionFilterProp="children"
|
||||||
|
value={lang}
|
||||||
|
onChange={setLang}
|
||||||
|
options={langOptions}
|
||||||
|
/>
|
||||||
|
<Button icon={<PlayCircleOutlined />} onClick={submitCode}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
|
||||||
|
const codeEditor = Editor({
|
||||||
|
mode: LangToMode[lang],
|
||||||
|
onChange: setCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const MiscPanel = <ProblemDetails action={funcMenu} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{msgContextHolder}
|
||||||
|
<Row justify="center" align="top" gutter={[16, 16]}>
|
||||||
|
<Col span={18}>{codeEditor}</Col>
|
||||||
|
<Col span={6}>{MiscPanel}</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,13 +1,28 @@
|
|||||||
import { HomePage } from "./pages/home.tsx";
|
import {
|
||||||
|
HomePage,
|
||||||
|
LoginPage,
|
||||||
|
LogoutPage,
|
||||||
|
ProblemPage,
|
||||||
|
SearchPage,
|
||||||
|
SubmitPage,
|
||||||
|
} from "./pages/pages.tsx";
|
||||||
|
import { RequireAuth } from "./components/auth.tsx";
|
||||||
|
|
||||||
import { ProblemLoader, ProblemPage } from "./pages/pages.tsx";
|
import { ProblemLoader } from "./api/loader.ts";
|
||||||
import { SearchPage } from "./pages/pages.tsx";
|
|
||||||
|
|
||||||
const RouteConfigs = [
|
const RouteConfigs = [
|
||||||
{
|
{
|
||||||
path: "home",
|
path: "home",
|
||||||
element: <HomePage />,
|
element: <HomePage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "login",
|
||||||
|
element: <LoginPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "logout",
|
||||||
|
element: <LogoutPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "search",
|
path: "search",
|
||||||
element: <SearchPage />,
|
element: <SearchPage />,
|
||||||
@ -19,7 +34,12 @@ const RouteConfigs = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "problem/:id/submit",
|
path: "problem/:id/submit",
|
||||||
element: <div>problem submit</div>,
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<SubmitPage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
loader: ProblemLoader,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ export default defineConfig({
|
|||||||
react(),
|
react(),
|
||||||
sentryVitePlugin({
|
sentryVitePlugin({
|
||||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||||
org: "ldcraft",
|
org: "0x7f",
|
||||||
project: "woj-ui",
|
project: "woj-ui",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
Reference in New Issue
Block a user