Files
car-crushers-portal/app/routes/events-team_.outstanding.tsx
Regalijan cfc57c838e
All checks were successful
Test, Build, Deploy / Test, Build, and Deploy (push) Successful in 55s
Test, Build, Deploy / Create Sentry Release (push) Successful in 6s
More events team nonsense
2026-04-11 03:30:46 -04:00

339 lines
8.2 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();
let year = now.getUTCFullYear();
if (month === 0) {
month = 12;
year--;
}
const data = await context.data.prisma.event.findMany({
orderBy: {
day: "asc",
},
select: {
answered_at: true,
day: true,
details: true,
id: true,
performed_at: true,
reached_minimum_player_count: true,
type: true,
},
where: {
AND: {
approved: true,
month,
year,
OR: [
{
AND: [
{
reached_minimum_player_count: false,
type: "gamenight",
},
],
},
],
},
},
});
return {
events: data,
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 (typeof events)[number]);
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: (typeof events)[number]) {
if (!event.performed_at) return "Approved";
if (event.type === "rotw" && event.answered_at) return "Solved";
if (event.type === "gamenight" && event.reached_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 = true;
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);
setEventData(
Object.defineProperty(newData, "performed_at", {
value: new Date().toISOString(),
}),
);
}
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);
setEventData(
Object.defineProperty(newData, "performed_at", {
value: new Date().toISOString(),
}),
);
}
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);
setEventData(
Object.defineProperty(newData, "performed_at", {
value: new Date().toISOString(),
}),
);
}
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={Boolean(eventData.performed_at)}
onClick={async () => await completed()}
>
Mark as Complete
</Button>
<Button
disabled={typeof eventData.performed_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={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.length > 100
? `${event.details.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>
</>
);
}