444 lines
13 KiB
TypeScript
444 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|