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" /> + } spacing="6"> USERNAME @@ -215,7 +346,7 @@ export default function() { - + {history} );