import { Box, Button, Card, CardBody, CardHeader, Container, Flex, Heading, HStack, Image, Input, InputGroup, InputRightElement, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Spacer, Stack, StackDivider, Text, useDisclosure, useToast, } from "@chakra-ui/react"; import { type FormEvent, type ReactElement, useState } from "react"; export async function loader({ context }: { context: RequestContext }) { const { current_user: currentUser } = context.data; if (!currentUser) throw new Response(null, { status: 401, }); if ( !(currentUser.permissions & (1 << 5)) && !(currentUser.permissions & (1 << 8)) ) throw new Response(null, { status: 403, }); return null; } export function meta() { return [{ title: "Hammer - Car Crushers" }]; } 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 [loading, setLoading] = useState(false); const { isOpen, onClose, onOpen } = useDisclosure(); const toast = useToast(); const ticketRegex = /https:\/\/carcrushers\.modmail\.dev\/logs\/[a-f\d]{12}$/; async function getHistory() { setVisible(false); setLoading(true); setHistory([]); if (queriedUsername.length < 4) { setLoading(false); return toast({ title: "Validation Error", description: `Username is too short`, status: "error", }); } const historyResp = await fetch( `/api/game-bans/${queriedUsername}/history`, ); if (!historyResp.ok) { setLoading(false); return toast({ title: "Failed To Fetch User", description: `${ ((await historyResp.json()) as { error: string }).error }`, status: "error", }); } const { history, user, }: { history: { [k: string]: any }[]; user: { avatar: string | null; current_status: string; id: number; name: string; }; } = await historyResp.json(); setAvatarUrl(user.avatar ?? "https://i.hep.gg/floppa"); setUid(user.id.toString()); setUsername(user.name); setStatus(user.current_status); const cardList = []; for (const entry of history) { const url = entry.evidence; const isUrl = () => { try { new URL(url).href; return true; } catch { return false; } }; cardList.push( {new Date(entry.executed_at).toLocaleString()} } spacing="4"> ACTION {entry.action} EVIDENCE {isUrl() ? ( {url} ) : ( url )} , ); } setHistory(cardList); setLoading(false); 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(ticketRegex) ? validIcon : invalidIcon} User Lookup Look up a user's punishment history here. { const { data }: { data?: string } & FormEvent = e; if (data?.match(/\W/)) e.preventDefault(); }} onChange={(e) => setQueriedUsername(e.target.value)} placeholder="Roblox username" /> } mt="8px" spacing="6"> USERNAME {username} USER ID {uid} MODERATION STATUS {status} {history} ); }