298 lines
7.6 KiB
TypeScript

import {
Box,
Button,
Container,
Flex,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
Select,
useBreakpointValue,
useDisclosure,
useToast,
VStack,
} from "@chakra-ui/react";
import { type ReactElement, useEffect, useState } from "react";
import AppealCard from "../../components/AppealCard.js";
import GameAppealCard from "../../components/GameAppealCard.js";
import NewGameBan from "../../components/NewGameBan.js";
import NewInfractionModal from "../../components/NewInfractionModal.js";
import ReportCard from "../../components/ReportCard.js";
import { useLoaderData } from "@remix-run/react";
import NewInactivityNotice from "../../components/NewInactivityNotice.js";
import InactivityNoticeCard from "../../components/InactivityNoticeCard.js";
export async function loader({ context }: { context: RequestContext }) {
const { current_user: currentUser } = context.data;
if (!currentUser)
throw new Response(null, {
status: 401,
});
const departments = {
DM: 1 << 2,
ET: 1 << 3,
FM: 1 << 10,
WM: 1 << 9,
};
const newItemPermissions = {
game_ban: [1 << 5],
inactivity: [1 << 2, 1 << 9, 1 << 10],
infraction: [1 << 0, 1 << 2, 1 << 6, 1 << 7],
};
const newItemNames: { [k: string]: string } = {
game_ban: "Game Ban",
inactivity: "Inactivity Notice",
infraction: "Infraction",
};
const typePermissions = {
appeal: [1 << 0, 1 << 1],
gma: [1 << 5],
report: [1 << 5],
};
const typeNames: { [k: string]: string } = {
appeal: "Discord Appeals",
gma: "Game Appeals",
report: "Game Reports",
};
const allowedNewItems = [];
const allowedTypes = [];
for (const [item, ints] of Object.entries(newItemPermissions)) {
if (ints.find((i) => currentUser.permissions & i))
allowedNewItems.push({ name: newItemNames[item], value: item });
}
for (const [type, ints] of Object.entries(typePermissions)) {
if (ints.find((i) => currentUser.permissions & i))
allowedTypes.push({ name: typeNames[type], value: type });
}
if (!allowedTypes.length)
throw new Response(null, {
status: 403,
});
return {
can_edit_ban_users: [
"165594923586945025",
"289372404541554689",
"396347223736057866",
].includes(currentUser.id),
departments: Object.entries(departments)
.filter((d) => d[1] & currentUser.permissions)
.map((arr) => arr[0]),
entry_types: allowedTypes,
item_types: allowedNewItems,
};
}
export function meta() {
return [
{
title: "Moderation Queue - Car Crushers",
},
];
}
export default function () {
const pageProps = useLoaderData<typeof loader>();
const isDesktop = useBreakpointValue({ base: false, lg: true });
const entryTypes = [];
const [entries, setEntries] = useState([] as ReactElement[]);
const [before, setBefore] = useState(Date.now());
for (const type of pageProps.entry_types)
entryTypes.push(
<option key={type.value} value={type.value}>
{type.name}
</option>,
);
async function updateQueue(
queue_type: string,
before: number,
show_closed = false,
): Promise<void> {
const queueReq = await fetch(
`/api/mod-queue/list?before=${before}&showClosed=${show_closed}&type=${queue_type}`,
);
if (!queueReq.ok) {
const errorData: { error: string } = await queueReq.json();
useToast()({
description: errorData.error,
duration: 10000,
isClosable: true,
status: "error",
title: "Failed to load queue",
});
return;
}
const searchParams = new URLSearchParams(location.search);
const itemId = searchParams.get("id");
const itemType = searchParams.get("type");
const entryData: { [k: string]: any }[] = await queueReq.json();
const newEntries = [...entries];
if (
itemId &&
itemType &&
["appeal", "gma", "inactivity", "report"].includes(itemType)
) {
const itemReq = await fetch(`/api/mod-queue/${itemType}/${itemId}`);
if (!itemReq.ok) {
useToast()({
description: ((await itemReq.json()) as { error: string }).error,
duration: 10000,
isClosable: true,
status: "error",
title: "Failed to load item with id " + itemId,
});
} else {
const itemData: { [k: string]: any } = await itemReq.json();
entryData.unshift(itemData);
}
}
if (!entryData.length) return;
for (const entry of entryData) {
switch (queue_type) {
case "appeal":
newEntries.push(<AppealCard {...(entry as AppealCardProps)} />);
break;
case "gma":
newEntries.push(<GameAppealCard {...(entry as GameAppealProps)} />);
break;
case "inactivity":
newEntries.push(
<InactivityNoticeCard {...(entry as InactivityNoticeProps)} />,
);
break;
case "report":
newEntries.push(<ReportCard {...(entry as ReportCardProps)} />);
break;
}
}
setEntries(newEntries);
setBefore(entryData[entryData.length - 1].created_at);
}
const itemModals: {
[k: string]: {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
[k: string]: any;
};
} = {
game_ban: useDisclosure(),
inactivity: useDisclosure(),
infraction: useDisclosure(),
};
useEffect(() => {
(async function () {
await updateQueue(pageProps.entry_types[0].value, before);
})();
const searchParams = new URLSearchParams(location.search);
const modal = searchParams.get("modal");
if (!modal || !pageProps.item_types.find((m) => m.value === modal)) return;
itemModals[modal].onOpen();
}, []);
return (
<Container maxW="container.lg">
<NewGameBan
isOpen={itemModals.game_ban.isOpen}
onClose={itemModals.game_ban.onClose}
/>
<NewInactivityNotice
departments={pageProps.departments}
isOpen={itemModals.inactivity.isOpen}
onClose={itemModals.inactivity.onClose}
/>
<NewInfractionModal
isOpen={itemModals.infraction.isOpen}
onClose={itemModals.infraction.onClose}
/>
<Flex>
<VStack w={isDesktop ? "container.md" : "container.lg"}>
{entries}
</VStack>
<Box display={isDesktop ? undefined : "none"} ml="16px" w="248px">
<Select>{entryTypes}</Select>
</Box>
</Flex>
<Popover placement="top-end">
<PopoverTrigger>
<Button
borderRadius="50%"
bottom="10vh"
h="16"
position="absolute"
right="10vh"
w="16"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
</svg>
</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>Create New</PopoverHeader>
<PopoverBody>
<VStack>
{pageProps.item_types.map((item) => (
<Button
key={item.value}
onClick={() => itemModals[item.value].onOpen()}
w="100%"
>
{item.name}
</Button>
))}
</VStack>
</PopoverBody>
</PopoverContent>
</Popover>
</Container>
);
}