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> ); }