304 lines
7.8 KiB
TypeScript
304 lines
7.8 KiB
TypeScript
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() + 1;
|
|
let year = now.getUTCFullYear();
|
|
|
|
if (month - 1 === 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);",
|
|
)
|
|
.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>
|
|
</>
|
|
);
|
|
}
|