Compare commits
4 Commits
c0acbd6ddc
...
5a5f7bdbbd
Author | SHA1 | Date | |
---|---|---|---|
5a5f7bdbbd | |||
d58aa4ba6e | |||
43f5423cf9 | |||
9dfe9bfe81 |
@ -22,6 +22,7 @@
|
|||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@reduxjs/toolkit": "^2.2.1",
|
"@reduxjs/toolkit": "^2.2.1",
|
||||||
|
"@tanstack/react-table": "^8.13.2",
|
||||||
"ace-builds": "^1.32.7",
|
"ace-builds": "^1.32.7",
|
||||||
"framer-motion": "^11.0.13",
|
"framer-motion": "^11.0.13",
|
||||||
"github-markdown-css": "^5.5.1",
|
"github-markdown-css": "^5.5.1",
|
||||||
|
@ -26,6 +26,9 @@ dependencies:
|
|||||||
'@reduxjs/toolkit':
|
'@reduxjs/toolkit':
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1(react-redux@9.1.0)(react@18.2.0)
|
version: 2.2.1(react-redux@9.1.0)(react@18.2.0)
|
||||||
|
'@tanstack/react-table':
|
||||||
|
specifier: ^8.13.2
|
||||||
|
version: 8.13.2(react-dom@18.2.0)(react@18.2.0)
|
||||||
ace-builds:
|
ace-builds:
|
||||||
specifier: ^1.32.7
|
specifier: ^1.32.7
|
||||||
version: 1.32.7
|
version: 1.32.7
|
||||||
@ -2374,6 +2377,23 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/react-table@8.13.2(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-b6mR3mYkjRtJ443QZh9sc7CvGTce81J35F/XMr0OoWbx0KIM7TTTdyNP2XKObvkLpYnLpCrYDwI3CZnLezWvpg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16'
|
||||||
|
react-dom: '>=16'
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/table-core': 8.13.2
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/table-core@8.13.2:
|
||||||
|
resolution: {integrity: sha512-/2saD1lWBUV6/uNAwrsg2tw58uvMJ07bO2F1IWMxjFRkJiXKQRuc3Oq2aufeobD3873+4oIM/DRySIw7+QsPPw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/babel__core@7.20.5:
|
/@types/babel__core@7.20.5:
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -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"],
|
tagTypes: ["User", "Status", "Submission", "ProblemInfo", "TaskInfo"],
|
||||||
endpoints: () => ({}),
|
endpoints: () => ({}),
|
||||||
});
|
});
|
||||||
|
98
src/app/services/problem.ts
Normal file
98
src/app/services/problem.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import type { Meta, WithCount, Wrap } from "./base";
|
||||||
|
import type { UserProfile } from "./user";
|
||||||
|
import { api } from "./api";
|
||||||
|
|
||||||
|
export interface RuntimeInfo {
|
||||||
|
MemoryLimit: number;
|
||||||
|
NProcLimit: number;
|
||||||
|
TimeLimit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskInfo {
|
||||||
|
Languages: {
|
||||||
|
Lang: string;
|
||||||
|
Runtime: {
|
||||||
|
Compile: RuntimeInfo;
|
||||||
|
Run: RuntimeInfo;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
Tasks: { Id: number; Points: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProblemInfo {
|
||||||
|
meta: Meta;
|
||||||
|
title: string;
|
||||||
|
statement: string;
|
||||||
|
tags: { Elements: string[] };
|
||||||
|
provider: UserProfile;
|
||||||
|
is_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateVersionRequest {
|
||||||
|
pid: number;
|
||||||
|
storage_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DetailRequest {
|
||||||
|
pid: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DetailResponse {
|
||||||
|
context: TaskInfo;
|
||||||
|
problem: ProblemInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchRequest {
|
||||||
|
keyword: string;
|
||||||
|
tag: string;
|
||||||
|
offset?: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchResponse = WithCount<ProblemInfo>;
|
||||||
|
|
||||||
|
export interface UpdateRequest {
|
||||||
|
pid: number;
|
||||||
|
title: string;
|
||||||
|
statement: string;
|
||||||
|
tags: string[];
|
||||||
|
is_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadResponse {
|
||||||
|
key: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const problemApi = api.injectEndpoints({
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
search: builder.query<Wrap<SearchResponse>, SearchRequest>({
|
||||||
|
query: (data: SearchRequest) => ({
|
||||||
|
url: "/problem/search",
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
providesTags: (result) =>
|
||||||
|
result && result.body.count > 0
|
||||||
|
? [
|
||||||
|
...result.body.data.map((p: ProblemInfo) => ({
|
||||||
|
type: "ProblemInfo" as const,
|
||||||
|
id: p.meta.ID,
|
||||||
|
})),
|
||||||
|
{ type: "ProblemInfo", id: "Search" },
|
||||||
|
]
|
||||||
|
: [{ type: "ProblemInfo", id: "Search" }],
|
||||||
|
}),
|
||||||
|
detail: builder.query<Wrap<DetailResponse>, DetailRequest>({
|
||||||
|
query: (data: DetailRequest) => ({
|
||||||
|
url: "/problem/search",
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
providesTags: (result) => (result ? [{ type: "TaskInfo", id: result.body.problem.meta.ID }] : []),
|
||||||
|
}),
|
||||||
|
// TODO: admin endpoints: version, update, upload
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useSearchQuery, useDetailQuery } = problemApi;
|
152
src/components/Table.tsx
Normal file
152
src/components/Table.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import type React from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
Select,
|
||||||
|
SkeletonText,
|
||||||
|
Table as ChakraTable,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Text,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
useColorModeValue,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import type { ColumnDef, PaginationState } from "@tanstack/react-table";
|
||||||
|
import { flexRender, getCoreRowModel, getFilteredRowModel, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { HiChevronDoubleLeft, HiChevronDoubleRight, HiChevronLeft, HiChevronRight } from "react-icons/hi";
|
||||||
|
|
||||||
|
interface TableProps<T> {
|
||||||
|
header?: React.ReactNode;
|
||||||
|
data: T[];
|
||||||
|
columns: ColumnDef<T>[];
|
||||||
|
total: number;
|
||||||
|
paging: PaginationState;
|
||||||
|
setPaging: (state: PaginationState | ((old: PaginationState) => PaginationState)) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Table<T>(props: TableProps<T>) {
|
||||||
|
const bg = useColorModeValue("gray.100", "gray.800");
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: props.data,
|
||||||
|
columns: props.columns,
|
||||||
|
rowCount: props.total,
|
||||||
|
state: { pagination: props.paging },
|
||||||
|
onPaginationChange: props.setPaging,
|
||||||
|
manualPagination: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { pageSize, pageIndex } = table.getState().pagination;
|
||||||
|
const pageCount = table.getPageCount().toLocaleString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card flexDirection="column" flex={1} maxWidth="100%" width="100%">
|
||||||
|
{props.header && <CardHeader>{props.header}</CardHeader>}
|
||||||
|
<SkeletonText
|
||||||
|
mx={3}
|
||||||
|
mb={3}
|
||||||
|
overflowX="scroll"
|
||||||
|
noOfLines={6}
|
||||||
|
spacing="4"
|
||||||
|
skeletonHeight="3"
|
||||||
|
isLoaded={!props.isLoading}
|
||||||
|
>
|
||||||
|
<ChakraTable>
|
||||||
|
<Thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<Tr key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<Th
|
||||||
|
key={header.id}
|
||||||
|
colSpan={header.colSpan}
|
||||||
|
bg={bg}
|
||||||
|
py={4}
|
||||||
|
px={6}
|
||||||
|
textTransform="none"
|
||||||
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</Th>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<Tr key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<Td key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</Td>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</ChakraTable>
|
||||||
|
<CardFooter justify="space-between" alignItems="center" p={3}>
|
||||||
|
<Flex flexDirection="row">
|
||||||
|
<IconButton
|
||||||
|
aria-label="goto to first page"
|
||||||
|
size="sm"
|
||||||
|
ml={4}
|
||||||
|
mr={2}
|
||||||
|
icon={<HiChevronDoubleLeft size={20} />}
|
||||||
|
onClick={() => table.firstPage()}
|
||||||
|
isDisabled={!table.getCanPreviousPage()}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label="go to previous page"
|
||||||
|
size="sm"
|
||||||
|
mr={2}
|
||||||
|
icon={<HiChevronLeft size={20} />}
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
isDisabled={!table.getCanPreviousPage()}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="center" alignItems="center">
|
||||||
|
<Text mr={4}>
|
||||||
|
Page {pageIndex + 1} of {pageCount}
|
||||||
|
</Text>
|
||||||
|
<Select w={28} value={pageSize} onChange={(e) => table.setPageSize(Number(e.target.value))}>
|
||||||
|
{[5, 10, 15, 20].map((pageSize) => (
|
||||||
|
<option key={pageSize} value={pageSize}>
|
||||||
|
Show {pageSize}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Flex>
|
||||||
|
<Flex flexDirection="row">
|
||||||
|
<IconButton
|
||||||
|
aria-label="go to next page"
|
||||||
|
size="sm"
|
||||||
|
ml={2}
|
||||||
|
icon={<HiChevronRight size={20} />}
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
isDisabled={!table.getCanNextPage()}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label="go to last page"
|
||||||
|
size="sm"
|
||||||
|
ml={2}
|
||||||
|
mr={4}
|
||||||
|
icon={<HiChevronDoubleRight size={20} />}
|
||||||
|
onClick={() => table.lastPage()}
|
||||||
|
isDisabled={!table.getCanNextPage()}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</CardFooter>
|
||||||
|
</SkeletonText>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
111
src/pages/ProblemListPage.tsx
Normal file
111
src/pages/ProblemListPage.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import type { ColumnDef, PaginationState } from "@tanstack/react-table";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
Link,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useColorModeValue,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { LinkIcon, SearchIcon } from "@chakra-ui/icons";
|
||||||
|
import { Link as ReactRouterLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import type { ProblemInfo } from "../app/services/problem";
|
||||||
|
import { useSearchQuery } from "../app/services/problem";
|
||||||
|
import Table from "../components/Table";
|
||||||
|
|
||||||
|
const Title = (props: { word: string }) => (
|
||||||
|
<Text as="b" fontSize="lg">
|
||||||
|
{props.word}{" "}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: ColumnDef<ProblemInfo>[] = [
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
accessorFn: (i, _) => i.meta.ID,
|
||||||
|
header: (_) => <Title word="ID" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "title",
|
||||||
|
header: (_) => <Title word="Problem" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tags",
|
||||||
|
accessorFn: (i, _) => i.tags.Elements,
|
||||||
|
header: (_) => <Title word="Tags" />,
|
||||||
|
cell: ({ cell }) => (
|
||||||
|
<Stack direction="row">
|
||||||
|
{(cell.getValue() as string[]).map((i) => (
|
||||||
|
<Badge key={i} variant="outline" colorScheme="teal">
|
||||||
|
{i}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "action",
|
||||||
|
accessorFn: (i, _) => i.meta.ID,
|
||||||
|
header: (_) => <Title word="Function" />,
|
||||||
|
cell: ({ cell }) => (
|
||||||
|
<Button variant="outline" colorScheme="teal" size="sm">
|
||||||
|
<Link as={ReactRouterLink} to={`/problem/${cell.getValue() as number}`}>
|
||||||
|
Details
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ProblemListPage() {
|
||||||
|
const [word, setWord] = useState("");
|
||||||
|
const [tag, setTag] = useState("");
|
||||||
|
const [paging, setPaging] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: problems, isFetching } = useSearchQuery({
|
||||||
|
keyword: word,
|
||||||
|
tag: tag,
|
||||||
|
offset: paging.pageIndex * paging.pageSize,
|
||||||
|
limit: paging.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconColor = useColorModeValue("gray.300", "gray.600");
|
||||||
|
const header = (
|
||||||
|
<Stack direction={["column", "row"]} spacing={5}>
|
||||||
|
<InputGroup>
|
||||||
|
<InputLeftElement>
|
||||||
|
<SearchIcon color={iconColor} />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input placeholder="Keyword" value={word} onChange={(e) => setWord(e.target.value)} />
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<InputLeftElement>
|
||||||
|
<LinkIcon color={iconColor} />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input placeholder="Tag" value={tag} onChange={(e) => setTag(e.target.value)} />
|
||||||
|
</InputGroup>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table<ProblemInfo>
|
||||||
|
header={header}
|
||||||
|
columns={columns}
|
||||||
|
data={problems?.body.data || []}
|
||||||
|
total={problems?.body.count || 0}
|
||||||
|
paging={paging}
|
||||||
|
setPaging={setPaging}
|
||||||
|
isLoading={isFetching}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -13,10 +13,10 @@ const SkeletonPage = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const Root = () => (
|
export const Root = () => (
|
||||||
<Flex direction="column" align="center" maxW={{ xl: "1200px" }} m="0 auto">
|
<Flex direction="column" align="center" maxW={{ xl: "1280px" }} m="0 auto">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<Container w="100%" flex="1" maxW="container.lg" py={2}>
|
<Container w="100%" flex="1" maxW="container.xl" py={2}>
|
||||||
<React.Suspense fallback={<SkeletonPage />}>
|
<React.Suspense fallback={<SkeletonPage />}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
|
@ -8,6 +8,7 @@ import { ErrorPage } from "./pages/ErrorPage";
|
|||||||
const HomePage = lazy(() => import("./pages/HomePage"));
|
const HomePage = lazy(() => import("./pages/HomePage"));
|
||||||
const LoginPage = lazy(() => import("./pages/LoginPage"));
|
const LoginPage = lazy(() => import("./pages/LoginPage"));
|
||||||
const LogoutPage = lazy(() => import("./pages/LogoutPage"));
|
const LogoutPage = lazy(() => import("./pages/LogoutPage"));
|
||||||
|
const ProblemListPage = lazy(() => import("./pages/ProblemListPage"));
|
||||||
|
|
||||||
export const router: RouteObject[] = [
|
export const router: RouteObject[] = [
|
||||||
{
|
{
|
||||||
@ -31,6 +32,10 @@ export const router: RouteObject[] = [
|
|||||||
path: "logout",
|
path: "logout",
|
||||||
element: <LogoutPage />,
|
element: <LogoutPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "problem",
|
||||||
|
element: <ProblemListPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user