295 lines
7.7 KiB
TypeScript

import {
Button,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Radio,
RadioGroup,
Table,
TableContainer,
Tbody,
Text,
Td,
Th,
Thead,
Tr,
VStack,
useToast,
} from "@chakra-ui/react";
import { useState } from "react";
export default function (props: { isOpen: boolean; onClose: () => void }) {
const actionMap: { [k: string]: number } = {};
const [users, setUsers] = useState([] as string[]);
const [loading, setLoading] = useState(false);
const toast = useToast();
const fileTypes: { [k: string]: string } = {
gif: "image/gif",
heic: "image/heic",
heif: "image/heif",
jfif: "image/jpeg",
jpeg: "image/jpeg",
jpg: "image/jpg",
m4v: "video/x-m4v",
mkv: "video/x-matroska",
mov: "video/mp4",
mp4: "video/mp4",
png: "image/png",
webp: "image/webp",
webm: "video/webm",
wmv: "video/x-ms-wmv",
};
function addUser(user: string) {
(document.getElementById("username") as HTMLInputElement).value = "";
const newUsers = [...users];
if (newUsers.includes(user)) return;
newUsers.push(user);
setUsers(newUsers);
}
function removeUser(user: string) {
const newUsers = [...users];
const userIdx = newUsers.indexOf(user);
if (userIdx === -1) return;
newUsers.splice(userIdx, 1);
setUsers(newUsers);
delete actionMap[user];
}
function reset() {
(document.getElementById("username") as HTMLInputElement).value = "";
(document.getElementById("evidence") as HTMLInputElement).value = "";
setUsers([]);
Object.keys(actionMap).forEach((k) => delete actionMap[k]);
props.onClose();
}
async function submit() {
setLoading(true);
const actions: number[] = [];
const usernames: string[] = [];
for (const [u, a] of Object.entries(actionMap)) {
actions.push(a);
usernames.push(u);
}
if (!usernames.length || !actions.length) return;
const files = (document.getElementById("evidence") as HTMLInputElement)
.files;
if (!files) {
setLoading(false);
return;
}
const [evidence] = files;
const submitReq = await fetch("/api/reports/submit", {
body: JSON.stringify({
actions,
bypass: true,
filename: evidence.name,
filesize: evidence.size,
usernames,
}),
headers: {
"content-type": "application/json",
},
method: "POST",
});
if (!submitReq.ok) {
setLoading(false);
toast({
description: ((await submitReq.json()) as { error: string }).error,
status: "error",
title: "Failed to submit report",
});
return;
}
const { id, upload_url }: { [k: string]: string } = await submitReq.json();
const fileUpload = await fetch(upload_url, {
body: evidence,
headers: {
"content-type":
evidence.type ||
fileTypes[
evidence.name.split(".")[evidence.name.split(".").length - 1]
],
},
method: "PUT",
});
if (!fileUpload.ok) {
setLoading(false);
await fetch("/api/reports/recall", {
body: JSON.stringify({ id }),
headers: {
"content-type": "application/json",
},
method: "POST",
});
toast({
description: "Failed to upload file",
status: "error",
title: "Error",
});
return;
}
await fetch("/api/reports/complete", {
body: JSON.stringify({ id }),
headers: {
"content-type": "application/json",
},
method: "POST",
});
toast({
description: "User moderated",
status: "success",
title: "Success",
});
setLoading(false);
props.onClose();
}
return (
<Modal isCentered isOpen={props.isOpen} onClose={props.onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>New Game Ban</ModalHeader>
<ModalCloseButton onClick={reset} />
<ModalBody>
<Text>Username(s)</Text>
<Input id="username" mb="8px" placeholder="builderman" />
<Button
onClick={function () {
const user = (
document.getElementById("username") as HTMLInputElement
).value;
if (
!user ||
user.length < 3 ||
user.length > 20 ||
// @ts-expect-error
user.match(/_/g)?.length > 1 ||
user.match(/\W/)
) {
toast({
description: "Check the username and try again",
duration: 5000,
isClosable: true,
status: "error",
title: "Invalid Username",
});
return;
}
addUser(user);
}}
>
Add
</Button>
<br />
<br />
<TableContainer>
<Table variant="simple">
<Thead>
<Tr>
<Th>Username</Th>
<Th>Punishment</Th>
<Th>&nbsp;</Th>
</Tr>
</Thead>
<Tbody>
{users.map((user) => (
<Tr key={user}>
<Td>{user}</Td>
<Td>
<RadioGroup
onChange={(val) => (actionMap[user] = parseInt(val))}
>
<VStack>
<Radio value="0">Do Nothing</Radio>
<Radio value="1">Hide from Leaderboards</Radio>
<Radio value="2">Ban</Radio>
</VStack>
</RadioGroup>
</Td>
<Td>
<Button onClick={() => removeUser(user)} variant="ghost">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
</svg>
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<br />
<br />
<Text>Evidence</Text>
<Button
mr="8px"
onClick={() => document.getElementById("evidence")?.click()}
>
Select Files
</Button>
<input id="evidence" type="file" />
</ModalBody>
<ModalFooter>
<Button onClick={reset}>Cancel</Button>
<Button
colorScheme="blue"
disabled={
!(
Object.entries(actionMap).length &&
(document.getElementById("evidence") as HTMLInputElement).files
?.length
)
}
ml="8px"
onClick={async () => await submit()}
isLoading={loading}
loadingText="Submitting..."
>
Submit
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}