274 lines
7.0 KiB
TypeScript
274 lines
7.0 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
Container,
|
|
Flex,
|
|
Popover,
|
|
PopoverArrow,
|
|
PopoverBody,
|
|
PopoverCloseButton,
|
|
PopoverContent,
|
|
PopoverHeader,
|
|
PopoverTrigger,
|
|
Select,
|
|
useBreakpointValue,
|
|
useDisclosure,
|
|
useToast,
|
|
VStack,
|
|
} from "@chakra-ui/react";
|
|
import { useEffect, useState } from "react";
|
|
import AppealCard from "../../components/AppealCard.js";
|
|
import GameAppealCard from "../../components/GameAppealCard.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";
|
|
|
|
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 JSX.Element[]);
|
|
const [before, setBefore] = useState(0);
|
|
|
|
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 = Date.now(),
|
|
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", "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 "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);
|
|
})();
|
|
|
|
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">
|
|
<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">
|
|
<PopoverTrigger>
|
|
<Button borderRadius="50%" h="16" 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>
|
|
);
|
|
}
|