419 lines
11 KiB
TypeScript
419 lines
11 KiB
TypeScript
import {
|
|
Button,
|
|
Container,
|
|
Divider,
|
|
Heading,
|
|
Link,
|
|
Modal,
|
|
ModalBody,
|
|
ModalCloseButton,
|
|
ModalContent,
|
|
ModalHeader,
|
|
ModalOverlay,
|
|
Table,
|
|
TableCaption,
|
|
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);
|
|
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();
|
|
|
|
if (etData) {
|
|
const now = new Date();
|
|
const pointsData = await context.env.D1.prepare(
|
|
"SELECT answered_at, approved, day, month, performed_at, reached_minimum_player_count, type, year FROM events WHERE created_by = ? AND month = ? AND year = ?;",
|
|
)
|
|
.bind(currentUser.id, now.getUTCMonth(), now.getUTCFullYear())
|
|
.all();
|
|
|
|
for (const row of pointsData.results as Record<string, any>[]) {
|
|
if (row.performed_at) etData.points += 10;
|
|
if (row.type === "gamenight" && row.reached_minimum_player_count)
|
|
etData.points += 10;
|
|
if (
|
|
row.type === "rotw" &&
|
|
row.answered_at - row.performed_at >= 86400000
|
|
)
|
|
etData.points += 10;
|
|
if (!row.performed_at && row.day < now.getUTCDate()) etData.points -= 5;
|
|
}
|
|
}
|
|
}
|
|
|
|
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>
|
|
</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>{data.etData?.name}</Tr>
|
|
<Tr>{data.etData?.points}</Tr>
|
|
<Tr>
|
|
<Link
|
|
href={`https://www.roblox.com/users/${data.etData?.roblox_id}/profile`}
|
|
>
|
|
{data.etData?.roblox_id}
|
|
</Link>
|
|
</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>
|
|
{result.open
|
|
? "Pending"
|
|
: result.approved
|
|
? "Approved"
|
|
: "Denied"}
|
|
</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>
|
|
);
|
|
}
|