Create revoke punishment modal

This commit is contained in:
2023-10-22 03:22:33 -04:00
parent 76cf568d9c
commit ed72f6fc9b

View File

@ -6,17 +6,28 @@ import {
Card, Card,
CardBody, CardBody,
CardHeader, CardHeader,
Center,
Container, Container,
Flex,
Heading, Heading,
HStack, HStack,
Image, Image,
Input, Input,
InputGroup,
InputRightElement,
Link, Link,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Spacer,
Stack, Stack,
StackDivider, StackDivider,
Text, Text,
useToast useDisclosure,
useToast,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { type FormEvent, type ReactElement, useState } from "react"; import { type FormEvent, type ReactElement, useState } from "react";
@ -25,7 +36,7 @@ export async function loader({ context }: { context: RequestContext }) {
if (!currentUser) if (!currentUser)
throw new Response(null, { throw new Response(null, {
status: 401 status: 401,
}); });
if ( if (
@ -33,7 +44,7 @@ export async function loader({ context }: { context: RequestContext }) {
!(currentUser.permissions & (1 << 8)) !(currentUser.permissions & (1 << 8))
) )
throw new Response(null, { throw new Response(null, {
status: 403 status: 403,
}); });
return null; return null;
@ -43,15 +54,18 @@ export function meta() {
return [{ title: "Hammer - Car Crushers" }]; return [{ title: "Hammer - Car Crushers" }];
} }
export default function() { export default function () {
const [queriedUsername, setQueriedUsername] = useState("");
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [uid, setUid] = useState(""); const [uid, setUid] = useState("");
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [avatarUrl, setAvatarUrl] = useState(""); const [avatarUrl, setAvatarUrl] = useState("");
const [ticketLink, setTicketLink] = useState("");
const [history, setHistory] = useState([] as ReactElement[]); const [history, setHistory] = useState([] as ReactElement[]);
const [hasResults, setHasResults] = useState(true); const [hasResults, setHasResults] = useState(true);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { isOpen, onClose, onOpen } = useDisclosure();
const toast = useToast(); const toast = useToast();
async function getHistory() { async function getHistory() {
@ -64,11 +78,13 @@ export default function() {
return toast({ return toast({
title: "Validation Error", title: "Validation Error",
description: `Username is too short`, description: `Username is too short`,
status: "error" status: "error",
}); });
} }
const historyResp = await fetch(`/api/game-bans/${username}/history`); const historyResp = await fetch(
`/api/game-bans/${queriedUsername}/history`,
);
if (!historyResp.ok) { if (!historyResp.ok) {
setLoading(false); setLoading(false);
@ -77,13 +93,13 @@ export default function() {
description: `${ description: `${
((await historyResp.json()) as { error: string }).error ((await historyResp.json()) as { error: string }).error
}`, }`,
status: "error" status: "error",
}); });
} }
const { const {
history, history,
user user,
}: { }: {
history: { [k: string]: any }[]; history: { [k: string]: any }[];
user: { avatar: string | null; id: number; name: string }; user: { avatar: string | null; id: number; name: string };
@ -92,10 +108,11 @@ export default function() {
if (!history.length) { if (!history.length) {
setLoading(false); setLoading(false);
setHasResults(false); setHasResults(false);
setStatus("");
return toast({ return toast({
title: "Nothing Found", title: "Nothing Found",
description: "This user doesn't have any moderation history.", description: "This user doesn't have any moderation history.",
status: "info" status: "info",
}); });
} }
@ -104,6 +121,7 @@ export default function() {
setAvatarUrl(user.avatar ?? "https://i.hep.gg/floppa"); setAvatarUrl(user.avatar ?? "https://i.hep.gg/floppa");
setUid(user.id.toString()); setUid(user.id.toString());
setUsername(user.name);
setStatus(history[history.length - 1].entity.properties.action.stringValue); setStatus(history[history.length - 1].entity.properties.action.stringValue);
for (const entry of history) { for (const entry of history) {
@ -123,7 +141,7 @@ export default function() {
<CardHeader> <CardHeader>
<Heading size="md"> <Heading size="md">
{new Date( {new Date(
parseInt(entry.entity.properties.executed_at.integerValue) parseInt(entry.entity.properties.executed_at.integerValue),
).toLocaleString()} ).toLocaleString()}
</Heading> </Heading>
</CardHeader> </CardHeader>
@ -150,7 +168,7 @@ export default function() {
</Stack> </Stack>
</CardBody> </CardBody>
</Card> </Card>
</Container> </Container>,
); );
} }
@ -159,8 +177,111 @@ export default function() {
setVisible(true); setVisible(true);
} }
const invalidIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
color="red"
>
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
</svg>
);
const validIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
color="green"
>
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z" />
</svg>
);
async function revokePunishment() {
const revokeResponse = await fetch(`/api/game-bans/${uid}/revoke`, {
body: JSON.stringify({ ticket_link: ticketLink }),
headers: {
"content-type": "application/json",
},
method: "POST",
});
if (!revokeResponse.ok) {
let error: string;
try {
error = ((await revokeResponse.json()) as { error: string }).error;
} catch {
error = "Unknown error";
}
toast({
description: error,
isClosable: true,
status: "error",
title: "Oops",
});
} else {
toast({
description: `Punishment revoked for ${username}`,
isClosable: true,
status: "success",
title: "Success",
});
}
onClose();
setTicketLink("");
}
return ( return (
<Container maxW="container.md"> <Container maxW="container.md">
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Revoke punishment for {username}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<InputGroup>
<Input
onChange={(e) => setTicketLink(e.target.value)}
placeholder="https://carcrushers.modmail.dev/logs/abcdef123456"
maxLength={49}
/>
<InputRightElement>
{ticketLink.match(
/https:\/\/carcrushers\.modmail\.dev\/logs\/[a-f\d]{12}$/,
)
? validIcon
: invalidIcon}
</InputRightElement>
</InputGroup>
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
onClose();
setTicketLink("");
}}
>
Cancel
</Button>
<Button
colorScheme="red"
mr="4px"
onClick={async () => await revokePunishment()}
>
Revoke
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Heading>User Lookup</Heading> <Heading>User Lookup</Heading>
<Text>Look up a user's punishment history here.</Text> <Text>Look up a user's punishment history here.</Text>
{!hasResults ? ( {!hasResults ? (
@ -178,7 +299,7 @@ export default function() {
if (data?.match(/\W/)) e.preventDefault(); if (data?.match(/\W/)) e.preventDefault();
}} }}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setQueriedUsername(e.target.value)}
placeholder="Roblox username" placeholder="Roblox username"
/> />
<Button <Button
@ -189,10 +310,20 @@ export default function() {
Search Search
</Button> </Button>
</HStack> </HStack>
<Center mb={3} mt={3}> <Container mb={3} mt={3}>
<Card visibility={visible ? "visible" : "hidden"}> <Card visibility={visible ? "visible" : "hidden"}>
<CardBody> <CardBody>
<Image mb="16" src={avatarUrl} /> <Flex>
<Image mb="16" src={avatarUrl} />
<Spacer />
<Button
colorScheme="red"
onClick={onOpen}
visibility={status ? "visible" : "hidden"}
>
Revoke Punishment
</Button>
</Flex>
<Stack divider={<StackDivider />} spacing="6"> <Stack divider={<StackDivider />} spacing="6">
<Box> <Box>
<Heading size="xs">USERNAME</Heading> <Heading size="xs">USERNAME</Heading>
@ -215,7 +346,7 @@ export default function() {
</Stack> </Stack>
</CardBody> </CardBody>
</Card> </Card>
</Center> </Container>
{history} {history}
</Container> </Container>
); );