Create revoke punishment modal
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user