import { Button, Container, Divider, Heading, Link, ListItem, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Stack, Table, TableCaption, TableContainer, Tbody, Td, Text, Th, Thead, Tr, UnorderedList, useDisclosure, useToast, } from "@chakra-ui/react"; import { type Dispatch, type SetStateAction, useEffect, useState } from "react"; import { useLoaderData } from "@remix-run/react"; export async function loader({ context }: { context: RequestContext }) { const { current_user: currentUser } = context.data; if (!currentUser) throw new Response(null, { status: 401 }); const d1Promises = []; for (const itemType of ["appeals", "inactivity_notices", "reports"]) d1Promises.push( context.env.D1.prepare( `SELECT * FROM ${itemType} WHERE json_extract(user, '$.id') = ? ORDER BY created_at DESC;`, ) .bind(currentUser.id) .all(), ); const settledPromises = await Promise.allSettled(d1Promises); let etData: { [k: string]: any } | null = null; if (currentUser.permissions & (1 << 3)) { etData = await context.env.D1.prepare( "SELECT name, points, roblox_id FROM et_members WHERE id = ?;", ) .bind(currentUser.id) .first(); } return { etData, items: settledPromises.map((p) => { if (p.status === "fulfilled") return p.value.results; return null; }) as any as ({ [k: string]: any }[] | null)[], permissions: currentUser.permissions as number, }; } export default function () { const data: { etData: { [k: string]: any } | null; items: ({ [k: string]: any }[] | null)[]; permissions: number; } = useLoaderData<typeof loader>(); const timeStates: { [k: number]: { data: string; set: Dispatch<SetStateAction<string>> }; } = {}; const toast = useToast(); for (const result of data.items) { if (!result) continue; for (const row of result) { const [data, set] = useState(new Date(row.created_at).toUTCString()); timeStates[row.created_at] = { data, set, }; useEffect(() => { timeStates[row.created_at].set( new Date(row.created_at).toLocaleString(), ); }, [row.created_at]); } } async function fetchItem(id: string, type: string) { const itemResp = await fetch(`/api/me/items/${type}/${id}`); if (!itemResp.ok) { let error: string; try { error = ((await itemResp.json()) as { error: string }).error; } catch { error = "Unknown error"; } toast({ description: error, isClosable: true, status: "error", title: "Oops", }); return; } const data: { [k: string]: any } = await itemResp.json(); switch (type) { case "appeal": setModalBody( <ModalContent> <ModalHeader>View Appeal</ModalHeader> <ModalCloseButton /> <ModalBody> <Heading size="lg">Why were you banned?</Heading> <br /> <Text> <i>{data.ban_reason}</i> </Text> <br /> <Divider /> <br /> <Heading size="lg">Why should we unban you?</Heading> <br /> <Text> <i>{data.reason_for_unban}</i> </Text> <br /> <Divider /> <br /> <Heading size="lg"> What have you learned from your mistake? </Heading> <br /> <Text> <i>{data.learned}</i> </Text> </ModalBody> </ModalContent>, ); break; case "inactivity": setModalBody( <ModalContent> <ModalHeader>View Inactivity Notice</ModalHeader> <ModalCloseButton /> <ModalBody> <Heading size="lg">Reason for Inactivity</Heading> <br /> <Text> <i>{data.reason}</i> </Text> <br /> <Divider /> <br /> <Heading size="lg">Start Date</Heading> <br /> <Text> <i>{new Date(data.start).toLocaleDateString()}</i> </Text> <br /> <Divider /> <br /> <Heading size="lg">End Date</Heading> <br /> <Text> <i>{new Date(data.end).toLocaleDateString()}</i> </Text> <br /> <Heading size="lg">Decisions</Heading> <br /> <UnorderedList> {data.departments.map((d: string) => { const dept = d as "DM" | "ET" | "FM" | "WM"; return ( <ListItem> <Stack alignItems="center" direction="row"> <Text>{d}: </Text> {typeof data.decisions[dept] === "boolean" ? ( data.decisions[dept] ? ( <svg fill="currentColor" height="16" viewBox="0 0 16 16" width="16" > <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" /> </svg> ) : ( <svg fill="currentColor" height="16" viewBox="0 0 16 16" width="16" > <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" /> </svg> ) ) : ( <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" > <path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z" /> </svg> )} </Stack> </ListItem> ); })} </UnorderedList> </ModalBody> </ModalContent>, ); break; case "report": setModalBody( <ModalContent> <ModalHeader>View Report</ModalHeader> <ModalCloseButton /> <ModalBody> <Heading size="lg">Username(s)</Heading> <br /> <Text> <i>{data.target_usernames.toString()}</i> </Text> <br /> <Divider /> <br /> <Heading size="lg">Description</Heading> <br /> <Text> <i>{data.description ?? "No description"}</i> </Text> <br /> <Divider /> <br /> <Heading size="lg">Media Links</Heading> <br /> {data.resolved_attachments.map((attachment: string) => ( <Link color="#646cff" href={attachment} target="_blank"> View media here </Link> ))} </ModalBody> </ModalContent>, ); break; default: setModalBody(<ModalContent></ModalContent>); break; } onOpen(); } const { isOpen, onClose, onOpen } = useDisclosure(); const [modalBody, setModalBody] = useState(<ModalContent></ModalContent>); function resetModal() { onClose(); setModalBody(<ModalContent></ModalContent>); } return ( <Container maxW="container.lg"> <Modal isCentered isOpen={isOpen} onClose={resetModal} size="lg"> <ModalOverlay /> {modalBody} </Modal> <Heading mb={8}>My Data</Heading> <br /> <br /> {data.permissions & (1 << 3) ? ( <> <Heading size="lg">Events Team Info</Heading> <TableContainer mb="16px"> <Table variant="simple"> <TableCaption> Reach out to ETM if this info is incorrect </TableCaption> <Thead> <Tr> <Th>Name</Th> <Th>Points</Th> <Th>Roblox ID</Th> </Tr> </Thead> <Tbody> <Tr> <Td>{data.etData?.name}</Td> <Td>{data.etData?.points}</Td> <Td> <Link href={`https://www.roblox.com/users/${data.etData?.roblox_id}`} > {data.etData?.roblox_id} </Link> </Td> </Tr> </Tbody> </Table> </TableContainer> </> ) : null} <Heading size="lg">Discord Appeals</Heading> <TableContainer mb="16px"> <Table variant="simple"> <Thead> <Tr> <Th>Date</Th> <Th>ID</Th> <Th>Status</Th> <Th>View</Th> </Tr> </Thead> <Tbody> {data.items[0]?.map((result) => { return ( <Tr> <Td>{timeStates[result.created_at].data}</Td> <Td>{result.id}</Td> <Td> {result.open ? "Pending" : typeof result.approved === "number" ? `${result.approved ? "Accepted" : "Denied"}` : "Unknown"} </Td> <Td> <Button onClick={async () => await fetchItem(result.id, "appeal")} > View </Button> </Td> </Tr> ); })} </Tbody> </Table> </TableContainer> <br /> {[1 << 2, 1 << 3, 1 << 9, 1 << 10].find((p) => data.permissions & p) ? ( <> <Heading size="lg">Inactivity Notices</Heading> <TableContainer mb="16px"> <Table variant="simple"> <Thead> <Tr> <Th>Date</Th> <Th>ID</Th> <Th>Status</Th> <Th>View</Th> </Tr> </Thead> <Tbody> {data.items[1]?.map((result) => { return ( <Tr> <Td>{timeStates[result.created_at].data}</Td> <Td>{result.id}</Td> <Td>Open for details</Td> <Td> <Button onClick={async () => await fetchItem(result.id, "inactivity") } > View </Button> </Td> </Tr> ); })} </Tbody> </Table> </TableContainer> <br /> </> ) : null} <Heading size="lg">Reports</Heading> <TableContainer> <Table variant="simple"> <Thead> <Tr> <Th>Date</Th> <Th>ID</Th> <Th>Status</Th> <Th>View</Th> </Tr> </Thead> <Tbody> {data.items[2]?.map((result) => { return ( <Tr> <Td>{timeStates[result.created_at].data}</Td> <Td>{result.id}</Td> <Td>{result.open ? "Pending" : "Reviewed"}</Td> <Td> <Button onClick={async () => await fetchItem(result.id, "report")} > View </Button> </Td> </Tr> ); })} </Tbody> </Table> </TableContainer> </Container> ); }