Skip to content

Commit 766de1b

Browse files
committed
Prepare for release
1 parent 8567496 commit 766de1b

23 files changed

+347
-88
lines changed

src/Router.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { Suspense, lazy } from "react";
22
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
33
import Layout from "@/layouts/Layout";
4-
import RankList from "./pages/RankList";
54

65
const Problem = lazy(() => import("@/pages/problem/Problem"));
76
const AdminProblemList = lazy(() => import("@/pages/admin/ProblemList"));
@@ -10,6 +9,7 @@ const AdminUserList = lazy(() => import("@/pages/admin/UserList"));
109
const ProblemList = lazy(() => import("@/pages/problem/ProblemList"));
1110
const JudgeList = lazy(() => import("@/pages//judge/JudgeList"));
1211
const Judge = lazy(() => import("@/pages/judge/Judge"));
12+
const RankList = lazy(() => import("@/pages/RankList"));
1313
const Login = lazy(() => import("@/pages/Login"));
1414

1515
const Router: React.FC = () => {
@@ -24,11 +24,11 @@ const Router: React.FC = () => {
2424
<Route path="" element={<ProblemList />} />
2525
<Route path=":slug" element={<Problem />} />
2626
</Route>
27+
<Route path="rank" element={<RankList />} />
2728
<Route path="judges">
2829
<Route path="" element={<JudgeList />} />
2930
<Route path=":uid" element={<Judge />} />
3031
</Route>
31-
<Route path="rank" element={<RankList />} />
3232
<Route path="login" element={<Login />} />
3333
{/* Admin */}
3434
<Route path="admin">

src/apis/judge.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ export async function postJudge(slug: string, code: string, language: string) {
1919
return res.data;
2020
}
2121

22-
export async function getJudgeList(limit?: number, offset?: number) {
22+
export async function getJudgeList(
23+
limit?: number,
24+
offset?: number,
25+
selfOnly?: boolean,
26+
) {
2327
limit = limit || 10;
2428
offset = offset || 0;
29+
selfOnly = selfOnly || false;
2530

2631
let res = await axiosClient.get<{
2732
total: number;
@@ -30,6 +35,7 @@ export async function getJudgeList(limit?: number, offset?: number) {
3035
params: {
3136
limit,
3237
offset,
38+
self_only: selfOnly,
3339
},
3440
});
3541
if (res.status !== 200) {

src/apis/user.ts

+47
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,50 @@ export async function getUserInfoList(
2525
}
2626
return res.data;
2727
}
28+
29+
export async function deleteUser(account: string) {
30+
let res = await axiosClient.delete(`/api/v1/user/${account}`);
31+
if (res.status !== 200) {
32+
throw Error("failed to delete user");
33+
}
34+
}
35+
36+
export async function grantUserRole(
37+
account: string,
38+
role: string,
39+
domain?: string,
40+
) {
41+
if (!domain) domain = "system";
42+
43+
let body = {
44+
role,
45+
domain,
46+
};
47+
let data = JSON.stringify(body);
48+
49+
let res = await axiosClient.post(`/api/v1/user/${account}/role`, data);
50+
if (res.status !== 200) {
51+
throw Error("failed to grant user role");
52+
}
53+
}
54+
55+
export async function revokeUserRole(
56+
account: string,
57+
role: string,
58+
domain?: string,
59+
) {
60+
if (!domain) domain = "system";
61+
62+
let body = {
63+
role,
64+
domain,
65+
};
66+
let data = JSON.stringify(body);
67+
68+
let res = await axiosClient.delete(`/api/v1/user/${account}/role`, {
69+
data,
70+
});
71+
if (res.status !== 200) {
72+
throw Error("failed to revoke user role");
73+
}
74+
}

src/components/control/ConfirmDialog.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ export const ConfirmDialog: FC<ConfirmDialogProps> = (props) => {
1313

1414
return (
1515
<dialog id={props.id} className="modal">
16-
<div className="modal-box">
16+
<div className="modal-box rounded">
1717
{props.title && <h3 className="text-lg font-bold">{t(props.title)}</h3>}
1818
<p className="py-4">{t(props.message)}</p>
1919
<div className="modal-action">
2020
<form method="dialog" className="flex gap-2">
21-
<button className="btn btn-sm">{t("Cancel")}</button>
21+
<button className="btn btn-sm rounded">{t("Cancel")}</button>
2222
<button
23-
className="btn btn-error btn-sm"
23+
className="btn btn-primary btn-sm rounded"
2424
onClick={props.onClickConfirm}
2525
>
2626
{t("Confirm")}

src/components/control/ProblemSearch.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const ProblemSearch: React.FC<ProblemSearchProps> = (props): JSX.Element => {
5050
)
5151
}
5252
>
53-
<option value="all">{t("all")}</option>
53+
<option value="all">{t("all difficulty")}</option>
5454
<option value="easy">{t("easy")}</option>
5555
<option value="medium">{t("medium")}</option>
5656
<option value="hard">{t("hard")}</option>

src/components/display/JudgeTable.tsx

+17-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import BrandCPPIcon from "@/components/display/icons/tabler/BrandCPPIcon";
77
import BrandPythonIcon from "@/components/display/icons/tabler/BrandPythonIcon";
88
import { shortenString } from "@/utils/string";
99
import UserAvatar from "./UserAvatar";
10+
import { useSelector } from "react-redux";
11+
import { userInfoSelector } from "@/store/selectors";
1012

1113
export interface JudgeTableProps {
1214
data: JudgeServiceModel.JudgeInfo[];
@@ -17,6 +19,7 @@ export interface JudgeTableProps {
1719
const JudgeTable: React.FC<JudgeTableProps> = (props) => {
1820
const { t } = useTranslation();
1921
const navigate = useNavigate();
22+
const userInfo = useSelector(userInfoSelector);
2023

2124
return (
2225
<div className={props.className}>
@@ -37,10 +40,19 @@ const JudgeTable: React.FC<JudgeTableProps> = (props) => {
3740
key={idx}
3841
className={joinClasses(
3942
props.data.length > 1 ? "border-base-content/10" : "border-0",
40-
props.enableRouting && "hover cursor-pointer",
43+
((props.enableRouting && userInfo?.roles?.includes("admin")) ||
44+
userInfo?.roles?.includes("super") ||
45+
userInfo?.account === judge.user?.account) &&
46+
"hover cursor-pointer",
4147
)}
4248
onClick={() => {
43-
if (props.enableRouting) navigate(judge.UID);
49+
if (
50+
(props.enableRouting && userInfo?.roles?.includes("admin")) ||
51+
userInfo?.roles?.includes("super") ||
52+
userInfo?.account === judge.user?.account
53+
) {
54+
navigate(`/judges/${judge.UID}`);
55+
}
4456
}}
4557
>
4658
<th>{shortenString(judge.UID, 8, false)}</th>
@@ -94,12 +106,12 @@ function getStatusBadageClass(status: string, verdict: string): string {
94106
if (status === "finished" && verdict === "Accepted") {
95107
return "bg-success/10 text-success";
96108
}
97-
if (status === "finished" && verdict === "WrongAnswer") {
98-
return "bg-error/10 text-error";
99-
}
100109
if (status === "finished" && verdict === "CompileError") {
101110
return "bg-warning/10 text-warning";
102111
}
112+
if (status === "finished") {
113+
return "bg-error/10 text-error";
114+
}
103115
if (status === "pending") {
104116
return "bg-primary/10 text-primary";
105117
}

src/components/display/ProblemTable.tsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const ProblemTable: React.FC<ProblemTableProps> = (props) => {
8888
<ConfirmDialog
8989
id="problem_delete_confirm_modal"
9090
title="Confirm"
91-
message="Are you sure to delete this problem?"
91+
message={t("Are you sure to delete this problem?")}
9292
onClickConfirm={() => {
9393
ProblemService.deleteProblem(deletingSlug).then((_) => {
9494
window.location.reload();
@@ -100,21 +100,25 @@ const ProblemTable: React.FC<ProblemTableProps> = (props) => {
100100
};
101101

102102
const ProblemTags: React.FC<{ tags: { name: string }[] }> = (props) => {
103+
const { t } = useTranslation();
104+
103105
return (
104-
<div className="space-x-2">
106+
<div>
105107
{props.tags.map((tag) => (
106108
<div
107109
key={tag.name}
108-
className="badge border-0 bg-base-300 font-semibold text-base-content/80"
110+
className="badge m-1 border-0 bg-base-300 font-semibold text-base-content/80"
109111
>
110-
{tag.name}
112+
{t(tag.name)}
111113
</div>
112114
))}
113115
</div>
114116
);
115117
};
116118

117119
const DifficultyBadge: React.FC<{ difficulty: string }> = (props) => {
120+
const { t } = useTranslation();
121+
118122
return (
119123
<div
120124
className={joinClasses(
@@ -124,7 +128,7 @@ const DifficultyBadge: React.FC<{ difficulty: string }> = (props) => {
124128
props.difficulty === "hard" && "bg-error/10 text-error",
125129
)}
126130
>
127-
{props.difficulty}
131+
{t(props.difficulty)}
128132
</div>
129133
);
130134
};

src/components/display/UserTable.tsx

+51-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import ConfirmDialog from "../control/ConfirmDialog";
66
import TrashIcon from "./icons/tabler/TrashIcon";
77
import { joinClasses } from "@/utils/common";
88
import UserCogIcon from "./icons/tabler/UserCogIcon";
9+
import * as UserService from "@/apis/user";
10+
import { useSelector } from "react-redux";
11+
import { userInfoSelector } from "@/store/selectors";
912

1013
export interface UserTableProps {
1114
data: UserServiceModel.UserInfo[];
@@ -15,6 +18,9 @@ export interface UserTableProps {
1518

1619
const UserTable: React.FC<UserTableProps> = (props) => {
1720
const { t } = useTranslation();
21+
const [deletingAccount, setDeletingAccount] = React.useState<string | null>(
22+
null,
23+
);
1824

1925
return (
2026
<>
@@ -47,7 +53,12 @@ const UserTable: React.FC<UserTableProps> = (props) => {
4753
</td>
4854
{props.showActions && (
4955
<td className="p-2">
50-
<UserActions userInfo={userInfo} onClickDelete={() => {}} />
56+
<UserActions
57+
userInfo={userInfo}
58+
onClickDelete={() => {
59+
setDeletingAccount(userInfo.account);
60+
}}
61+
/>
5162
</td>
5263
)}
5364
</tr>
@@ -58,9 +69,17 @@ const UserTable: React.FC<UserTableProps> = (props) => {
5869
<ConfirmDialog
5970
id="user_delete_confirm_modal"
6071
title="Confirm"
61-
message="Are you sure to delete this user?"
72+
message={t("Are you sure to delete this user?")}
6273
onClickConfirm={() => {
63-
window.location.reload();
74+
if (deletingAccount) {
75+
UserService.deleteUser(deletingAccount)
76+
.then(() => {
77+
window.location.reload();
78+
})
79+
.catch((err) => {
80+
console.error(err);
81+
});
82+
}
6483
}}
6584
/>
6685
</>
@@ -92,24 +111,50 @@ interface ActionsProps {
92111
}
93112

94113
const UserActions: React.FC<ActionsProps> = (props) => {
114+
const userInfo = useSelector(userInfoSelector);
115+
95116
const onClickDelete = (e: React.MouseEvent) => {
96117
e.preventDefault();
97118
e.stopPropagation();
98119

99120
props.onClickDelete();
100121
const modal = document.getElementById(
101-
"delete_confirm_modal",
122+
"user_delete_confirm_modal",
102123
) as HTMLDialogElement;
103124
modal?.showModal();
104125
};
105126

106127
return (
107128
<div className="flex space-x-1">
108-
<button className="btn btn-square btn-ghost btn-sm rounded">
129+
<button
130+
className={joinClasses(
131+
"btn btn-square btn-ghost btn-sm rounded",
132+
(props.userInfo.roles?.includes("super") ||
133+
props.userInfo.account === userInfo?.account) &&
134+
"btn-disabled",
135+
)}
136+
onClick={() => {
137+
if (props.userInfo.roles?.includes("admin")) {
138+
UserService.revokeUserRole(props.userInfo.account, "admin").catch(
139+
(err) => {
140+
console.error(err);
141+
},
142+
);
143+
} else if (!props.userInfo.roles?.includes("super")) {
144+
UserService.grantUserRole(props.userInfo.account, "admin").catch(
145+
(err) => {
146+
console.error(err);
147+
},
148+
);
149+
}
150+
window.location.reload();
151+
}}
152+
>
109153
<UserCogIcon
110154
className={joinClasses(
111155
"h-5 w-5",
112-
props.userInfo.roles?.includes("admin")
156+
props.userInfo.roles?.includes("admin") ||
157+
props.userInfo.roles?.includes("super")
113158
? "text-success"
114159
: "text-base-content",
115160
)}

src/components/navigation/PageBreadcrumbs.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const PageBreadcrumbs: React.FC = () => {
77
let paths = location.pathname.split("/").filter((x) => x);
88

99
return (
10-
<div className="breadcrumbs overflow-visible text-sm">
10+
<div className="breadcrumbs overflow-visible p-0 text-sm">
1111
<ul>
1212
{paths.map((path, index) => {
1313
const url = `/${paths.slice(0, index + 1).join("/")}`;

src/hooks/judge.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,11 @@ export const useJudgeList = () => {
6464
const [total, setTotal] = useState<number>(0);
6565
const [limit, setLimit] = useState<number>(10);
6666
const [offset, setOffset] = useState<number>(0);
67+
const [selfOnly, setSelfOnly] = useState<boolean>(false);
6768
const [judgeList, setJudgeList] = useState<JudgeServiceModel.JudgeInfo[]>([]);
6869

6970
const getJudgeListFromServer = () => {
70-
JudgeService.getJudgeList()
71+
JudgeService.getJudgeList(limit, offset, selfOnly)
7172
.then((res) => {
7273
setJudgeList(res.list);
7374
setTotal(res.total);
@@ -86,7 +87,7 @@ export const useJudgeList = () => {
8687
});
8788
};
8889

89-
useEffect(getJudgeListFromServer, [dispatch, limit, offset, t]);
90+
useEffect(getJudgeListFromServer, [dispatch, limit, offset, t, selfOnly]);
9091

9192
function getJudgeList() {
9293
return judgeList;
@@ -105,5 +106,16 @@ export const useJudgeList = () => {
105106
setOffset(offset);
106107
}
107108

108-
return { getJudgeList, refreshJudgeList, getPageCount, setPagenation };
109+
function getSelfOnly() {
110+
return selfOnly;
111+
}
112+
113+
return {
114+
getJudgeList,
115+
refreshJudgeList,
116+
getPageCount,
117+
setPagenation,
118+
getSelfOnly,
119+
setSelfOnly,
120+
};
109121
};

0 commit comments

Comments
 (0)