356 lines
9.3 KiB
TypeScript
356 lines
9.3 KiB
TypeScript
import {
|
|
Button,
|
|
Container,
|
|
Divider,
|
|
Heading,
|
|
Link,
|
|
Modal,
|
|
ModalBody,
|
|
ModalCloseButton,
|
|
ModalContent,
|
|
ModalHeader,
|
|
ModalOverlay,
|
|
Table,
|
|
TableContainer,
|
|
Tbody,
|
|
Td,
|
|
Text,
|
|
Th,
|
|
Thead,
|
|
Tr,
|
|
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 user = ?
|
|
ORDER BY created_at DESC;`,
|
|
)
|
|
.bind(currentUser.id)
|
|
.all(),
|
|
);
|
|
|
|
const settledPromises = await Promise.allSettled(d1Promises);
|
|
|
|
return {
|
|
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: {
|
|
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>
|
|
</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 />
|
|
<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 === "boolean"
|
|
? `${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>
|
|
{result.open
|
|
? "Pending"
|
|
: Object.values(result.decisions).find((d) => !d)
|
|
? "Denied"
|
|
: "Approved"}
|
|
</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>
|
|
);
|
|
}
|