diff --git a/app/routes/hammer.tsx b/app/routes/hammer.tsx
index 9c388ec..cd48d06 100644
--- a/app/routes/hammer.tsx
+++ b/app/routes/hammer.tsx
@@ -6,17 +6,28 @@ import {
Card,
CardBody,
CardHeader,
- Center,
Container,
+ Flex,
Heading,
HStack,
Image,
Input,
+ InputGroup,
+ InputRightElement,
Link,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Spacer,
Stack,
StackDivider,
Text,
- useToast
+ useDisclosure,
+ useToast,
} from "@chakra-ui/react";
import { type FormEvent, type ReactElement, useState } from "react";
@@ -25,7 +36,7 @@ export async function loader({ context }: { context: RequestContext }) {
if (!currentUser)
throw new Response(null, {
- status: 401
+ status: 401,
});
if (
@@ -33,7 +44,7 @@ export async function loader({ context }: { context: RequestContext }) {
!(currentUser.permissions & (1 << 8))
)
throw new Response(null, {
- status: 403
+ status: 403,
});
return null;
@@ -43,15 +54,18 @@ export function meta() {
return [{ title: "Hammer - Car Crushers" }];
}
-export default function() {
+export default function () {
+ const [queriedUsername, setQueriedUsername] = useState("");
const [username, setUsername] = useState("");
const [uid, setUid] = useState("");
const [status, setStatus] = useState("");
const [visible, setVisible] = useState(false);
const [avatarUrl, setAvatarUrl] = useState("");
+ const [ticketLink, setTicketLink] = useState("");
const [history, setHistory] = useState([] as ReactElement[]);
const [hasResults, setHasResults] = useState(true);
const [loading, setLoading] = useState(false);
+ const { isOpen, onClose, onOpen } = useDisclosure();
const toast = useToast();
async function getHistory() {
@@ -64,11 +78,13 @@ export default function() {
return toast({
title: "Validation Error",
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) {
setLoading(false);
@@ -77,13 +93,13 @@ export default function() {
description: `${
((await historyResp.json()) as { error: string }).error
}`,
- status: "error"
+ status: "error",
});
}
const {
history,
- user
+ user,
}: {
history: { [k: string]: any }[];
user: { avatar: string | null; id: number; name: string };
@@ -92,10 +108,11 @@ export default function() {
if (!history.length) {
setLoading(false);
setHasResults(false);
+ setStatus("");
return toast({
title: "Nothing Found",
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");
setUid(user.id.toString());
+ setUsername(user.name);
setStatus(history[history.length - 1].entity.properties.action.stringValue);
for (const entry of history) {
@@ -123,7 +141,7 @@ export default function() {
{new Date(
- parseInt(entry.entity.properties.executed_at.integerValue)
+ parseInt(entry.entity.properties.executed_at.integerValue),
).toLocaleString()}
@@ -150,7 +168,7 @@ export default function() {
-
+ ,
);
}
@@ -159,8 +177,111 @@ export default function() {
setVisible(true);
}
+ const invalidIcon = (
+
+ );
+
+ const validIcon = (
+
+ );
+
+ 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 (
+
+
+
+ Revoke punishment for {username}
+
+
+
+ setTicketLink(e.target.value)}
+ placeholder="https://carcrushers.modmail.dev/logs/abcdef123456"
+ maxLength={49}
+ />
+
+ {ticketLink.match(
+ /https:\/\/carcrushers\.modmail\.dev\/logs\/[a-f\d]{12}$/,
+ )
+ ? validIcon
+ : invalidIcon}
+
+
+
+
+
+
+
+
+
User Lookup
Look up a user's punishment history here.
{!hasResults ? (
@@ -178,7 +299,7 @@ export default function() {
if (data?.match(/\W/)) e.preventDefault();
}}
- onChange={(e) => setUsername(e.target.value)}
+ onChange={(e) => setQueriedUsername(e.target.value)}
placeholder="Roblox username"
/>
{history}
);