import { Alert, AlertIcon, Box, Button, Heading, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, StackDivider, Table, TableCaption, TableContainer, Tbody, Td, Th, Thead, Tr, useDisclosure, useToast, VStack, } from "@chakra-ui/react"; import { useLoaderData } from "@remix-run/react"; import { useEffect, useState } from "react"; export async function loader({ context }: { context: RequestContext }) { const now = new Date(); let month = now.getUTCMonth(); let year = now.getUTCFullYear(); if (month === 0) { month = 12; year--; } const data = await context.env.D1.prepare( "SELECT day, details, id FROM events WHERE approved = 1 AND month = ? AND year = ? AND (performed_at IS NULL OR (reached_minimum_player_count = 0 AND type = 'gamenight')) ORDER BY day;", ) .bind(month, year) .all(); return { events: data.results as Record<string, string | number>[], past_cutoff: now.getUTCDate() > 7, }; } export default function () { const { events, past_cutoff } = useLoaderData<typeof loader>(); const { isOpen, onClose, onOpen } = useDisclosure(); const [eventData, setEventData] = useState({} as { [k: string]: any }); const [isBrowserSupported, setIsBrowserSupported] = useState(true); const toast = useToast(); useEffect(() => { if (typeof structuredClone === "undefined") setIsBrowserSupported(false); }, []); async function displayErrorToast(response: Response, title: string) { let msg = "Unknown error"; try { msg = ((await response.json()) as { error: string }).error; } catch {} toast({ description: msg, status: "error", title, }); } function getStatus(event: { [k: string]: string | number }) { if (!event.performed_at) return "Approved"; if (event.type === "rotw" && event.answered_at) return "Solved"; if (event.type === "gamenight" && event.areached_minimum_player_count) return "Certified"; return "Completed"; } async function certify() { const response = await fetch( `/api/events-team/events/${eventData.id}/certify`, { body: "{}", headers: { "content-type": "application/json", }, method: "POST", }, ); if (!response.ok) { await displayErrorToast(response, "Failed to certify game night"); return; } toast({ status: "success", title: "Game night certified", }); const newData = structuredClone(eventData); newData.reached_minimum_player_count = 1; setEventData(newData); } async function completed() { const response = await fetch( `/api/events-team/events/${eventData.id}/complete`, { body: "{}", headers: { "content-type": "application/json", }, method: "POST", }, ); if (!response.ok) { await displayErrorToast(response, "Failed to mark as completed"); return; } toast({ status: "success", title: "Event marked as complete", }); const newData = structuredClone(eventData); newData.performed_at = Date.now(); setEventData(newData); } async function forgotten() { const response = await fetch( `/api/events-team/events/${eventData.id}/forgotten`, { body: "{}", headers: { "content-type": "application/json", }, method: "POST", }, ); if (!response.ok) { await displayErrorToast(response, "Failed to mark as forgotten"); return; } toast({ title: "Event marked as forgotten", status: "success", }); const newData = structuredClone(eventData); newData.performed_at = 0; setEventData(newData); } async function solve() { const response = await fetch( `/api/events-team/events/${eventData.id}/solve`, { body: "{}", headers: { "content-type": "application/json", }, method: "POST", }, ); if (!response.ok) { await displayErrorToast(response, "Failed to mark as solved"); return; } toast({ status: "success", title: "Riddle marked as solved", }); const newData = structuredClone(eventData); newData.answered_at = Date.now(); setEventData(newData); } return ( <> <Alert display={past_cutoff ? undefined : "none"} status="warning"> <AlertIcon /> The cutoff period for retroactively actioning events has passed. </Alert> <Alert display={isBrowserSupported ? "none" : undefined} status="error"> <AlertIcon /> This browser is unsupported. Please upgrade to a browser not several years out of date. </Alert> <Modal isOpen={isOpen} onClose={onClose}> <ModalOverlay /> <ModalContent> <ModalHeader>Action Menu</ModalHeader> <ModalCloseButton /> <ModalBody> <VStack divider={<StackDivider />}> <Box gap="8px"> <Heading size="xs">Completion</Heading> <Button disabled={typeof eventData.completed_at === "number"} onClick={async () => await completed()} > Mark as Complete </Button> <Button disabled={typeof eventData.completed_at === "number"} onClick={async () => await forgotten()} > Mark as Forgotten </Button> </Box> {eventData.type === "rotw" ? ( <Box gap="8px"> <Heading size="xs">Solved Status</Heading> <Button disabled={Boolean(eventData.answered_at)} onClick={async () => await solve()} > {eventData.answered_at ? "Solved" : "Mark as Solved"} </Button> </Box> ) : null} {eventData.type === "gamenight" ? ( <Box gap="8px"> <Heading size="xs">Certified Status</Heading> <Button disabled={Boolean(eventData.reached_minimum_player_count)} onClick={async () => await certify()} > {eventData.reached_minimum_player_count ? "Certified" : "Certify"} </Button> </Box> ) : null} </VStack> </ModalBody> <ModalFooter> <Button onClick={onClose}>Close</Button> </ModalFooter> </ModalContent> </Modal> <TableContainer> <Table variant="simple"> <TableCaption> Events that are not denied or left pending which need to be actioned </TableCaption> <Thead> <Tr> <Th>Day</Th> <Th>Details</Th> <Th>Current Status</Th> <Th>Action</Th> </Tr> </Thead> <Tbody> {events.map((event) => ( <Tr> <Td>{event.day}</Td> <Td> {(event.details as string).length > 100 ? `${(event.details as string).substring(0, 97)}...` : event.details} </Td> <Td>{getStatus(event)}</Td> <Td> <Button disabled={past_cutoff} onClick={() => { setEventData(event); onOpen(); }} > Action Menu </Button> </Td> </Tr> ))} </Tbody> </Table> </TableContainer> </> ); }