Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
car-crushers-portal/app/routes/events-team_.outstanding.tsx
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
303 lines (271 sloc)
7.83 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
</> | |
); | |
} |