Compare commits
12 Commits
9274b3314d
...
a75881c877
Author | SHA1 | Date | |
---|---|---|---|
a75881c877 | |||
57b62c335a | |||
e311719239 | |||
3f3a75f507 | |||
32d96207b9 | |||
3b2c311143 | |||
d6810f1867 | |||
9c7dbd5a34 | |||
1c69063825 | |||
2816c9fbee | |||
5a85de3268 | |||
8b2e0ad894 |
@ -3,4 +3,5 @@ node_modules/
|
|||||||
/pnpm-lock.yaml
|
/pnpm-lock.yaml
|
||||||
/postcss.config.cjs
|
/postcss.config.cjs
|
||||||
|
|
||||||
src/components/Languages.tsx
|
src/components/Languages.ts
|
||||||
|
src/components/Verdict.ts
|
||||||
|
@ -15,6 +15,6 @@ const baseQuery = fetchBaseQuery({
|
|||||||
|
|
||||||
export const api = createApi({
|
export const api = createApi({
|
||||||
baseQuery: retry(baseQuery, { maxRetries: 2 }),
|
baseQuery: retry(baseQuery, { maxRetries: 2 }),
|
||||||
tagTypes: ["User", "Status", "Submission", "ProblemInfo", "TaskInfo"],
|
tagTypes: ["User", "Status", "Submission", "ProblemInfo", "TaskInfo", "Status"],
|
||||||
endpoints: () => ({}),
|
endpoints: () => ({}),
|
||||||
});
|
});
|
||||||
|
96
src/app/services/status.ts
Normal file
96
src/app/services/status.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import type { Meta, WithCount, Wrap } from "./base";
|
||||||
|
import type { SubmissionInfo } from "./submission";
|
||||||
|
import { api } from "./api";
|
||||||
|
|
||||||
|
export enum Verdict {
|
||||||
|
Accepted,
|
||||||
|
WrongAnswer,
|
||||||
|
JuryFailed,
|
||||||
|
PartialCorrect,
|
||||||
|
TimeLimitExceeded,
|
||||||
|
MemoryLimitExceeded,
|
||||||
|
RuntimeError,
|
||||||
|
CompileError,
|
||||||
|
SystemError,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskResultInfo {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
points: number;
|
||||||
|
runtime: {
|
||||||
|
real_time: number;
|
||||||
|
cpu_time: number;
|
||||||
|
memory: number;
|
||||||
|
};
|
||||||
|
verdict: Verdict;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextInfo {
|
||||||
|
message: string;
|
||||||
|
compile_message: string;
|
||||||
|
tasks: TaskResultInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusInfo {
|
||||||
|
meta: Meta;
|
||||||
|
submission: SubmissionInfo;
|
||||||
|
problem_version_id: number;
|
||||||
|
context: ContextInfo;
|
||||||
|
point: number;
|
||||||
|
is_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmissionWithPoint {
|
||||||
|
submission: SubmissionInfo;
|
||||||
|
point: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryRequest {
|
||||||
|
pid?: number;
|
||||||
|
uid?: number;
|
||||||
|
offset?: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryResponse = WithCount<SubmissionWithPoint>;
|
||||||
|
|
||||||
|
export interface QueryBySubmissionRequest {
|
||||||
|
sid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryByVersionRequest {
|
||||||
|
pvid: number;
|
||||||
|
offset?: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryByVersionResponse {
|
||||||
|
count: number;
|
||||||
|
status: StatusInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const statusApi = api.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
list: builder.query<Wrap<QueryResponse>, QueryRequest>({
|
||||||
|
query: (data: QueryRequest) => ({
|
||||||
|
url: "/status/query",
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
providesTags: [{ type: "Status", id: "Search" }],
|
||||||
|
}),
|
||||||
|
status: builder.query<Wrap<StatusInfo>, QueryBySubmissionRequest>({
|
||||||
|
query: (data: QueryBySubmissionRequest) => ({
|
||||||
|
url: "/status/query/submission",
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
// NOTE: we're tracing submission ID here, when rejudge is fired, clear it
|
||||||
|
providesTags: (result) => (result ? [{ type: "Status", id: result.body.submission.meta.ID }] : []),
|
||||||
|
}),
|
||||||
|
// TODO: admin endpoints: detailByVersion
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useListQuery, useStatusQuery } = statusApi;
|
@ -29,7 +29,7 @@ export interface UserProfile {
|
|||||||
|
|
||||||
export const userApi = api.injectEndpoints({
|
export const userApi = api.injectEndpoints({
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
create: builder.mutation<Wrap<string>, UserRequest>({
|
register: builder.mutation<Wrap<string>, UserRequest>({
|
||||||
query: (data: UserRequest) => ({
|
query: (data: UserRequest) => ({
|
||||||
url: "/user/create",
|
url: "/user/create",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -63,4 +63,4 @@ export const userApi = api.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { useCreateMutation, useLoginMutation, useLogoutMutation, useProfileQuery } = userApi;
|
export const { useRegisterMutation, useLoginMutation, useLogoutMutation, useProfileQuery } = userApi;
|
||||||
|
14
src/components/AccordionTitleItem.tsx
Normal file
14
src/components/AccordionTitleItem.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { AccordionButton, AccordionIcon, Box } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
export default function TitleItem({ word }: { word: string }) {
|
||||||
|
return (
|
||||||
|
<h2>
|
||||||
|
<AccordionButton>
|
||||||
|
<Box as="span" flex="1" textAlign="left">
|
||||||
|
{word}
|
||||||
|
</Box>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
34
src/components/Highlight.tsx
Normal file
34
src/components/Highlight.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Code, useColorModeValue } from "@chakra-ui/react";
|
||||||
|
import { Highlight as PrismHighlight, themes } from "prism-react-renderer";
|
||||||
|
|
||||||
|
interface HighlightProps {
|
||||||
|
lang: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Highlight(props: HighlightProps) {
|
||||||
|
const codeTheme = useColorModeValue(themes.oneLight, themes.oneDark);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrismHighlight language={props.lang} code={props.code} theme={codeTheme}>
|
||||||
|
{({ style, tokens, getLineProps, getTokenProps }) => (
|
||||||
|
<Code
|
||||||
|
padding={2}
|
||||||
|
rounded="md"
|
||||||
|
display="block"
|
||||||
|
whiteSpace="pre"
|
||||||
|
backgroundColor={style.backgroundColor}
|
||||||
|
overflow="auto"
|
||||||
|
>
|
||||||
|
{tokens.map((line, i) => (
|
||||||
|
<div key={i} {...getLineProps({ line })}>
|
||||||
|
{line.map((token, key) => (
|
||||||
|
<span key={key} {...getTokenProps({ token })} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Code>
|
||||||
|
)}
|
||||||
|
</PrismHighlight>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Code, useColorModeValue } from "@chakra-ui/react";
|
import { lazy } from "react";
|
||||||
|
import { Code } from "@chakra-ui/react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { Highlight, themes } from "prism-react-renderer";
|
|
||||||
|
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import remarkMath from "remark-math";
|
import remarkMath from "remark-math";
|
||||||
@ -11,13 +11,13 @@ import rehypeMathJaxSvg from "rehype-mathjax";
|
|||||||
import "github-markdown-css";
|
import "github-markdown-css";
|
||||||
import "./Markdown.css";
|
import "./Markdown.css";
|
||||||
|
|
||||||
|
const Highlight = lazy(() => import("./Highlight"));
|
||||||
|
|
||||||
interface MarkdownProps {
|
interface MarkdownProps {
|
||||||
markdown: string;
|
markdown: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Markdown(props: MarkdownProps) {
|
export default function Markdown(props: MarkdownProps) {
|
||||||
const codeTheme = useColorModeValue(themes.oneLight, themes.oneDark);
|
|
||||||
|
|
||||||
const remarkPlugins = [remarkGfm, emoji, remarkMath];
|
const remarkPlugins = [remarkGfm, emoji, remarkMath];
|
||||||
const rehypePlugins = [rehypeRaw, rehypeMathJaxSvg];
|
const rehypePlugins = [rehypeRaw, rehypeMathJaxSvg];
|
||||||
|
|
||||||
@ -34,28 +34,7 @@ export default function Markdown(props: MarkdownProps) {
|
|||||||
const lang = (match || ["", "text"])[1];
|
const lang = (match || ["", "text"])[1];
|
||||||
const code = String(children).replace(/\n$/, "");
|
const code = String(children).replace(/\n$/, "");
|
||||||
|
|
||||||
const codeBlock = (
|
const codeBlock = <Highlight lang={lang} code={code} />;
|
||||||
<Highlight language={lang} code={code} theme={codeTheme}>
|
|
||||||
{({ style, tokens, getLineProps, getTokenProps }) => (
|
|
||||||
<Code
|
|
||||||
padding={2}
|
|
||||||
rounded="md"
|
|
||||||
display="block"
|
|
||||||
whiteSpace="pre"
|
|
||||||
backgroundColor={style.backgroundColor}
|
|
||||||
overflow="auto"
|
|
||||||
>
|
|
||||||
{tokens.map((line, i) => (
|
|
||||||
<div key={i} {...getLineProps({ line })}>
|
|
||||||
{line.map((token, key) => (
|
|
||||||
<span key={key} {...getTokenProps({ token })} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Code>
|
|
||||||
)}
|
|
||||||
</Highlight>
|
|
||||||
);
|
|
||||||
const codeInline = <Code>{children}</Code>;
|
const codeInline = <Code>{children}</Code>;
|
||||||
return !inline && match ? codeBlock : codeInline;
|
return !inline && match ? codeBlock : codeInline;
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { lazy, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionButton,
|
|
||||||
AccordionIcon,
|
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionPanel,
|
AccordionPanel,
|
||||||
Box,
|
|
||||||
Link,
|
Link,
|
||||||
Select,
|
Select,
|
||||||
Table,
|
Table,
|
||||||
@ -19,17 +16,9 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Link as ReactRouterLink } from "react-router-dom";
|
import { Link as ReactRouterLink } from "react-router-dom";
|
||||||
import type { DetailResponse, RuntimeInfo } from "../app/services/problem";
|
import type { DetailResponse, RuntimeInfo } from "../app/services/problem";
|
||||||
|
import { LanguageMap } from "./Languages";
|
||||||
|
|
||||||
const TitleItem = ({ word }: { word: string }) => (
|
const TitleItem = lazy(() => import("../components/AccordionTitleItem"));
|
||||||
<h2>
|
|
||||||
<AccordionButton>
|
|
||||||
<Box as="span" flex="1" textAlign="left">
|
|
||||||
{word}
|
|
||||||
</Box>
|
|
||||||
<AccordionIcon />
|
|
||||||
</AccordionButton>
|
|
||||||
</h2>
|
|
||||||
);
|
|
||||||
|
|
||||||
interface ProblemInfoMenuProps {
|
interface ProblemInfoMenuProps {
|
||||||
data?: DetailResponse;
|
data?: DetailResponse;
|
||||||
@ -76,7 +65,7 @@ export default function ProblemInfoMenu(props: ProblemInfoMenuProps) {
|
|||||||
{props.data?.context.Languages.map((l) => (
|
{props.data?.context.Languages.map((l) => (
|
||||||
<WrapItem key={l.Lang}>
|
<WrapItem key={l.Lang}>
|
||||||
<Tag variant="outline" size="sm" colorScheme="pink">
|
<Tag variant="outline" size="sm" colorScheme="pink">
|
||||||
{l.Lang}
|
{LanguageMap[l.Lang].label}
|
||||||
</Tag>
|
</Tag>
|
||||||
</WrapItem>
|
</WrapItem>
|
||||||
))}
|
))}
|
||||||
@ -122,7 +111,7 @@ export default function ProblemInfoMenu(props: ProblemInfoMenuProps) {
|
|||||||
<Select size="sm" onChange={(e) => setLang(e.target.value)} value={lang}>
|
<Select size="sm" onChange={(e) => setLang(e.target.value)} value={lang}>
|
||||||
{props.data?.context.Languages.map((l) => (
|
{props.data?.context.Languages.map((l) => (
|
||||||
<option key={l.Lang} value={l.Lang}>
|
<option key={l.Lang} value={l.Lang}>
|
||||||
{l.Lang}
|
{LanguageMap[l.Lang].label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
116
src/components/SubmissionTable.tsx
Normal file
116
src/components/SubmissionTable.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Badge, Button, Center, Link } from "@chakra-ui/react";
|
||||||
|
import type { ColumnDef, PaginationState } from "@tanstack/react-table";
|
||||||
|
import { Link as ReactRouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import type { SubmissionWithPoint } from "../app/services/status";
|
||||||
|
import { useListQuery } from "../app/services/status";
|
||||||
|
import type { UserProfile } from "../app/services/user";
|
||||||
|
import Table from "./Table";
|
||||||
|
import Title from "./Title";
|
||||||
|
import type { SubmissionInfo } from "../app/services/submission";
|
||||||
|
import type { ProblemInfo } from "../app/services/problem";
|
||||||
|
|
||||||
|
const columns: ColumnDef<SubmissionWithPoint>[] = [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
accessorFn: (i, _) => i.submission.meta.ID,
|
||||||
|
header: (_) => <Title word="ID" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "title",
|
||||||
|
accessorFn: (i, _) => i.submission.problem,
|
||||||
|
header: (_) => <Title word="Problem" />,
|
||||||
|
cell: ({ cell }) => {
|
||||||
|
const problem = cell.getValue() as ProblemInfo;
|
||||||
|
return (
|
||||||
|
<Link as={ReactRouterLink} to={`/problem/${problem.meta.ID}`} color="teal.500">
|
||||||
|
{problem.title}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "user",
|
||||||
|
accessorFn: (i, _) => i.submission.user,
|
||||||
|
header: (_) => <Title word="User" />,
|
||||||
|
cell: ({ cell }) => {
|
||||||
|
const user = cell.getValue() as UserProfile;
|
||||||
|
return (
|
||||||
|
<Link as={ReactRouterLink} to={`/profile/${user.meta.ID}`} color="teal.500">
|
||||||
|
{user.nick_name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "point",
|
||||||
|
accessorKey: "point",
|
||||||
|
header: (_) => <Title word="Point" center={true} />,
|
||||||
|
cell: ({ cell }) => {
|
||||||
|
const point = cell.getValue() as number;
|
||||||
|
const color = point === -1 ? "purple" : point === 100 ? "green" : "red";
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Badge colorScheme={color} fontSize="md">
|
||||||
|
{point}
|
||||||
|
</Badge>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "date",
|
||||||
|
accessorFn: (i, _) => new Date(i.submission.meta.CreatedAt).toLocaleString(),
|
||||||
|
header: (_) => <Title word="Date" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "action",
|
||||||
|
accessorKey: "submission",
|
||||||
|
header: (_) => <Title word="Action" center={true} />,
|
||||||
|
cell: ({ cell }) => {
|
||||||
|
const submission = cell.getValue() as SubmissionInfo;
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Button variant="outline" colorScheme="teal" size="sm">
|
||||||
|
<Link as={ReactRouterLink} to={`/status/${submission.meta.ID}`}>
|
||||||
|
Detail Status
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface SubmissionTableProps {
|
||||||
|
pid?: number;
|
||||||
|
uid?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SubmissionTable(props: SubmissionTableProps) {
|
||||||
|
const [paging, setPaging] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const { data: info, isFetching } = useListQuery({
|
||||||
|
pid: props.pid || 0,
|
||||||
|
uid: props.uid || 0,
|
||||||
|
offset: paging.pageIndex * paging.pageSize,
|
||||||
|
limit: paging.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table<SubmissionWithPoint>
|
||||||
|
header={<Title word="Judge Status" />}
|
||||||
|
columns={columns}
|
||||||
|
data={info?.body.data || []}
|
||||||
|
total={info?.body.count || 0}
|
||||||
|
paging={paging}
|
||||||
|
setPaging={setPaging}
|
||||||
|
isLoading={isFetching}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
10
src/components/Title.tsx
Normal file
10
src/components/Title.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Center, Text } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
export default function Title(props: { word: string; center?: boolean }) {
|
||||||
|
const b = (
|
||||||
|
<Text as="b" fontSize="lg">
|
||||||
|
{props.word}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
return props.center ? <Center>{b}</Center> : b;
|
||||||
|
}
|
17
src/components/Verdict.ts
Normal file
17
src/components/Verdict.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Verdict } from "../app/services/status";
|
||||||
|
|
||||||
|
/* eslint-disable prettier/prettier */
|
||||||
|
export const VerdictMap: {
|
||||||
|
[key in Verdict]: { color: string; label: string; name: string };
|
||||||
|
} = {
|
||||||
|
[Verdict.Accepted]: { color: "#52C41A", label: "AC", name: "Accepted" },
|
||||||
|
[Verdict.WrongAnswer]: { color: "#E74C3C", label: "WA", name: "Wrong Answer" },
|
||||||
|
[Verdict.JuryFailed]: { color: "#0E1D69", label: "JF", name: "Jury Failed" },
|
||||||
|
[Verdict.PartialCorrect]: { color: "#E67E22", label: "PC", name: "Partial Correct" },
|
||||||
|
[Verdict.TimeLimitExceeded]: { color: "#052242", label: "TLE", name: "Time Limit Exceeded" },
|
||||||
|
[Verdict.MemoryLimitExceeded]: { color: "#052242", label: "MLE", name: "Memory Limit Exceeded" },
|
||||||
|
[Verdict.RuntimeError]: { color: "#9D3DCF", label: "RE", name: "Runtime Error" },
|
||||||
|
[Verdict.CompileError]: { color: "#FADB14", label: "CE", name: "Compile Error" },
|
||||||
|
[Verdict.SystemError]: { color: "#0E1D69", label: "UKE", name: "System Error" },
|
||||||
|
};
|
||||||
|
/* eslint-enable prettier/prettier */
|
@ -43,8 +43,9 @@ export const authSlice = createAppSlice({
|
|||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
return { ...initialState, token: null };
|
return { ...initialState, token: null };
|
||||||
})
|
})
|
||||||
.addMatcher(userApi.endpoints.profile.matchRejected, (_state, _action) => {
|
.addMatcher(userApi.endpoints.profile.matchRejected, (_state, action) => {
|
||||||
// Profile Failed
|
// Profile Failed
|
||||||
|
if (action.meta.arg.originalArgs != 0) return;
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
return { ...initialState, token: null };
|
return { ...initialState, token: null };
|
||||||
});
|
});
|
||||||
|
@ -30,9 +30,9 @@ export default function ProblemDetailPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
leftIcon={<MdSearch />}
|
leftIcon={<MdSearch />}
|
||||||
onClick={() => navigate(`/problem/${id}/status`)}
|
onClick={() => navigate(`/problem/${id}/submission`)}
|
||||||
>
|
>
|
||||||
Status
|
Submissions
|
||||||
</Button>
|
</Button>
|
||||||
</WrapItem>
|
</WrapItem>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
@ -44,7 +44,7 @@ export default function ProblemDetailPage() {
|
|||||||
<Box w="100%">
|
<Box w="100%">
|
||||||
<Markdown markdown={details?.body.problem.statement || ""} />
|
<Markdown markdown={details?.body.problem.statement || ""} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box w="480px">
|
<Box maxW="480px">
|
||||||
<ProblemInfoMenu data={details?.body} action={actionBtn} />
|
<ProblemInfoMenu data={details?.body} action={actionBtn} />
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1,28 +1,13 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { ColumnDef, PaginationState } from "@tanstack/react-table";
|
import type { ColumnDef, PaginationState } from "@tanstack/react-table";
|
||||||
import {
|
import { Badge, Button, Input, InputGroup, InputLeftElement, Link, Stack, useColorModeValue } from "@chakra-ui/react";
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputLeftElement,
|
|
||||||
Link,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
useColorModeValue,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { LinkIcon, SearchIcon } from "@chakra-ui/icons";
|
import { LinkIcon, SearchIcon } from "@chakra-ui/icons";
|
||||||
import { Link as ReactRouterLink } from "react-router-dom";
|
import { Link as ReactRouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import type { ProblemInfo } from "../app/services/problem";
|
import type { ProblemInfo } from "../app/services/problem";
|
||||||
import { useSearchQuery } from "../app/services/problem";
|
import { useSearchQuery } from "../app/services/problem";
|
||||||
import Table from "../components/Table";
|
import Table from "../components/Table";
|
||||||
|
import Title from "../components/Title";
|
||||||
const Title = (props: { word: string }) => (
|
|
||||||
<Text as="b" fontSize="lg">
|
|
||||||
{props.word}{" "}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns: ColumnDef<ProblemInfo>[] = [
|
const columns: ColumnDef<ProblemInfo>[] = [
|
||||||
{
|
{
|
||||||
|
52
src/pages/ProfilePage.tsx
Normal file
52
src/pages/ProfilePage.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { lazy } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Accordion, AccordionItem, AccordionPanel, Box, Stack, Table, Tbody, Td, Tr } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { useProfileQuery, UserRole } from "../app/services/user";
|
||||||
|
|
||||||
|
const SubmissionTable = lazy(() => import("../components/SubmissionTable"));
|
||||||
|
const TitleItem = lazy(() => import("../components/AccordionTitleItem"));
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const uid = Number(id) || 0;
|
||||||
|
const { data: profile } = useProfileQuery(uid);
|
||||||
|
const roleName = Object.entries(UserRole).filter(([, v]) => v === profile?.body.role);
|
||||||
|
|
||||||
|
const userInfoMenu = (
|
||||||
|
<Accordion defaultIndex={[0]}>
|
||||||
|
<AccordionItem>
|
||||||
|
<TitleItem word="Profile" />
|
||||||
|
<AccordionPanel>
|
||||||
|
<Table variant="striped" size="sm">
|
||||||
|
<Tbody>
|
||||||
|
<Tr>
|
||||||
|
<Td>Nickname</Td>
|
||||||
|
<Td>{profile?.body.nick_name}</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>Joined at</Td>
|
||||||
|
<Td>{new Date(profile?.body.meta.CreatedAt || "").toLocaleString()}</Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td>Role</Td>
|
||||||
|
<Td>{(roleName.length > 0 && roleName[0][0]) || "N/A"}</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction={{ base: "column", md: "row" }}>
|
||||||
|
<Box w="100%">
|
||||||
|
<SubmissionTable uid={uid} />
|
||||||
|
</Box>
|
||||||
|
<Box maxW="480px">{userInfoMenu}</Box>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
150
src/pages/StatusPage.tsx
Normal file
150
src/pages/StatusPage.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import { lazy } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionPanel,
|
||||||
|
Alert,
|
||||||
|
AlertIcon,
|
||||||
|
Box,
|
||||||
|
Spinner,
|
||||||
|
Stack,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
Wrap,
|
||||||
|
WrapItem,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { useStatusQuery } from "../app/services/status";
|
||||||
|
import { useDetailQuery } from "../app/services/problem";
|
||||||
|
import { LanguageMap } from "../components/Languages";
|
||||||
|
import { VerdictMap } from "../components/Verdict";
|
||||||
|
|
||||||
|
const ProblemInfoMenu = lazy(() => import("../components/ProblemInfoMenu"));
|
||||||
|
const TitleItem = lazy(() => import("../components/AccordionTitleItem"));
|
||||||
|
const Highlight = lazy(() => import("../components/Highlight"));
|
||||||
|
|
||||||
|
const gridStyle: React.CSSProperties = {
|
||||||
|
width: "5.3em",
|
||||||
|
height: "5.3em",
|
||||||
|
margin: "0.2em",
|
||||||
|
fontSize: "1.5em",
|
||||||
|
padding: "0.5em",
|
||||||
|
textAlign: "center",
|
||||||
|
textShadow: "1px 1px 2px gray",
|
||||||
|
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 StatusPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const sid = Number(id) || 0;
|
||||||
|
const { data: status } = useStatusQuery({ sid: sid });
|
||||||
|
const { data: problem } = useDetailQuery({ pid: status?.body.submission.problem.meta.ID || 0 });
|
||||||
|
|
||||||
|
const showSourceCode = status && status.body.submission.code !== "";
|
||||||
|
const showMessage = status && (status.body.context.message !== "" || status.body.context.compile_message !== "");
|
||||||
|
|
||||||
|
const emptyTab = (
|
||||||
|
<Alert status="warning">
|
||||||
|
<AlertIcon />
|
||||||
|
<Spinner size="sm" mr={2} />
|
||||||
|
<Text as="b" mr={2}>
|
||||||
|
Waiting for Judge...
|
||||||
|
</Text>
|
||||||
|
<Text>Please refresh the page after a few seconds.</Text>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusCardTab = (
|
||||||
|
<Wrap spacing="0.3em">
|
||||||
|
{status?.body.context.tasks.map((task) => (
|
||||||
|
<WrapItem key={task.id}>
|
||||||
|
<Tooltip label={task.message}>
|
||||||
|
<Box style={{ ...gridStyle, background: VerdictMap[task.verdict].color }}>
|
||||||
|
<div style={topLeftStyle}>{`#${task.id}`}</div>
|
||||||
|
<div>
|
||||||
|
<div style={labelStyle}>{VerdictMap[task.verdict].label}</div>
|
||||||
|
<div style={statusStyle}>
|
||||||
|
{`${Math.max(
|
||||||
|
task.runtime.real_time,
|
||||||
|
task.runtime.cpu_time,
|
||||||
|
)}ms/${task.runtime.memory}KB`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</WrapItem>
|
||||||
|
))}
|
||||||
|
</Wrap>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceCodeTab = (
|
||||||
|
<Highlight
|
||||||
|
lang={!showSourceCode ? "text" : LanguageMap[status.body.submission.language].prism}
|
||||||
|
code={status?.body.submission.code || ""}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageTab = (
|
||||||
|
<Accordion defaultIndex={[0, 1]} allowMultiple>
|
||||||
|
<AccordionItem>
|
||||||
|
<TitleItem word="Message" />
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<Highlight lang="text" code={status?.body.context.message || "Not Available"} />
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem>
|
||||||
|
<TitleItem word="Compile Message" />
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<Highlight lang="text" code={status?.body.context.compile_message || "Not Available"} />
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction={{ base: "column", md: "row" }}>
|
||||||
|
<Box w="100%">
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab>Task Info</Tab>
|
||||||
|
<Tab isDisabled={!showSourceCode}>Source Code</Tab>
|
||||||
|
<Tab isDisabled={!showMessage}>Message</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel>{status ? statusCardTab : emptyTab}</TabPanel>
|
||||||
|
<TabPanel>{sourceCodeTab}</TabPanel>
|
||||||
|
<TabPanel>{messageTab}</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
<Box maxW="480px">
|
||||||
|
<ProblemInfoMenu data={problem?.body} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
27
src/pages/SubmissionListPage.tsx
Normal file
27
src/pages/SubmissionListPage.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { lazy } from "react";
|
||||||
|
import { Box, Stack } from "@chakra-ui/react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useDetailQuery } from "../app/services/problem";
|
||||||
|
|
||||||
|
const SubmissionTable = lazy(() => import("../components/SubmissionTable"));
|
||||||
|
const ProblemInfoMenu = lazy(() => import("../components/ProblemInfoMenu"));
|
||||||
|
|
||||||
|
export default function SubmissionListPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const pid = Number(id) || 0;
|
||||||
|
const { data: details } = useDetailQuery({ pid: pid });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction={{ base: "column", md: "row" }}>
|
||||||
|
<Box w="100%">
|
||||||
|
<SubmissionTable pid={pid} />
|
||||||
|
</Box>
|
||||||
|
<Box maxW="480px">
|
||||||
|
<ProblemInfoMenu data={details?.body} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -26,7 +26,7 @@ export default function SubmitPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
navigate(`/problem/${id}/status`);
|
navigate(`/problem/${id}/submission`);
|
||||||
} else if (result.isError) {
|
} else if (result.isError) {
|
||||||
toast({
|
toast({
|
||||||
status: "error",
|
status: "error",
|
||||||
@ -56,7 +56,7 @@ export default function SubmitPage() {
|
|||||||
<Box w="100%">
|
<Box w="100%">
|
||||||
<Editor mode={LanguageMap[lang].mode} onChange={setCode} />
|
<Editor mode={LanguageMap[lang].mode} onChange={setCode} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box w="480px">
|
<Box maxW="480px">
|
||||||
<ProblemInfoMenu data={details?.body} onLanguageSelect={setLang} action={actionBtn} />
|
<ProblemInfoMenu data={details?.body} onLanguageSelect={setLang} action={actionBtn} />
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -11,6 +11,9 @@ const LogoutPage = lazy(() => import("./pages/LogoutPage"));
|
|||||||
const ProblemListPage = lazy(() => import("./pages/ProblemListPage"));
|
const ProblemListPage = lazy(() => import("./pages/ProblemListPage"));
|
||||||
const ProblemDetailPage = lazy(() => import("./pages/ProblemDetailPage"));
|
const ProblemDetailPage = lazy(() => import("./pages/ProblemDetailPage"));
|
||||||
const SubmitPage = lazy(() => import("./pages/SubmitPage"));
|
const SubmitPage = lazy(() => import("./pages/SubmitPage"));
|
||||||
|
const SubmissionListPage = lazy(() => import("./pages/SubmissionListPage"));
|
||||||
|
const StatusPage = lazy(() => import("./pages/StatusPage"));
|
||||||
|
const ProfilePage = lazy(() => import("./pages/ProfilePage"));
|
||||||
|
|
||||||
export const router: RouteObject[] = [
|
export const router: RouteObject[] = [
|
||||||
{
|
{
|
||||||
@ -50,6 +53,38 @@ export const router: RouteObject[] = [
|
|||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "problem/:id/submission",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<SubmissionListPage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "status/:id",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<StatusPage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "profile",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<ProfilePage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "profile/:id",
|
||||||
|
element: (
|
||||||
|
<RequireAuth>
|
||||||
|
<ProfilePage />
|
||||||
|
</RequireAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user