import { Box, Button, Card, CardBody, CardFooter, Container, Flex, FormControl, FormLabel, Heading, IconButton, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Stack, StackDivider, Switch, Text, useDisclosure, useToast, VStack, } from "@chakra-ui/react"; import { useLoaderData } from "@remix-run/react"; import { useState } from "react"; import calendarStyles from "react-big-calendar/lib/css/react-big-calendar.css"; import { type LinksFunction } from "@remix-run/cloudflare"; export const links: LinksFunction = () => { return [{ href: calendarStyles, rel: "stylesheet" }]; }; export async function loader({ context }: { context: RequestContext }) { if (!context.data.current_user) throw new Response(null, { status: 401, }); if ( ![1 << 3, 1 << 4, 1 << 12].find( (p) => context.data.current_user.permissions & p, ) ) throw new Response(null, { status: 403, }); const now = new Date(); const monthEventList = await context.env.D1.prepare( "SELECT answer, approved, created_by, day, details, id, month, pending, performed_at, reached_minimum_player_count, type, year FROM events WHERE month = ? AND year = ? ORDER BY day ASC;", ) .bind(now.getUTCMonth() + 1, now.getUTCFullYear()) .all(); if (monthEventList.error) throw new Response(null, { status: 500, }); const membersList = await context.env.D1.prepare( "SELECT id, name FROM et_members WHERE id IN (SELECT created_by FROM events WHERE month = ? AND year = ?);", ) .bind(now.getUTCMonth() + 1, now.getUTCFullYear()) .all(); if (membersList.error) throw new Response(null, { status: 500, }); return { can_approve: Boolean( [1 << 4, 1 << 12].find((p) => context.data.current_user.permissions & p), ), events: monthEventList.results, members: membersList.results as { id: string; name: string }[], user_id: context.data.current_user.id as string, }; } export default function () { const { can_approve, events, members, user_id, }: { can_approve: boolean; events: { [k: string]: any }[]; members: { id: string; name: string }[]; user_id: string; } = useLoaderData(); const [eventData, setEventData] = useState(events); const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen: isCompleteOpen, onClose: closeComplete, onOpen: openComplete, } = useDisclosure(); const { isOpen: isAnsweredOpen, onClose: closeAnswered, onOpen: openAnswered, } = useDisclosure(); const { isOpen: isForgottenOpen, onClose: onForgottenClose, onOpen: onForgottenOpen, } = useDisclosure(); const { isOpen: isDeleteOpen, onClose: onDeleteClose, onOpen: onDeleteOpen, } = useDisclosure(); const { isOpen: isRescheduleOpen, onClose: onRescheduleClose, onOpen: onRescheduleOpen, } = useDisclosure(); const toast = useToast(); const [selectedEvent, setSelectedEvent] = useState(""); const [showOld, setShowOld] = useState(false); async function decide(approved: boolean, eventId: string) { const decisionResp = await fetch( `/api/events-team/events/${eventId}/decision`, { body: JSON.stringify({ approved }), headers: { "content-type": "application/json", }, method: "POST", }, ); if (!decisionResp.ok) { let errorMsg = "Unknown error"; try { errorMsg = ((await decisionResp.json()) as { error: string }).error; } catch {} toast({ description: errorMsg, status: "error", title: "Oops!", }); return; } toast({ description: `Event ${approved ? "approved" : "rejected"}`, status: "success", title: "Success", }); const newEventData = eventData; const eventIdx = eventData.findIndex((e) => e.id === eventId); newEventData[eventIdx].approved = approved; newEventData[eventIdx].pending = false; setEventData([...newEventData]); } async function certify(eventId: string) { const certifyResp = await fetch( `/api/events-team/events/${eventId}/certify`, { body: "{}", headers: { "content-type": "application/json", }, method: "POST", }, ); if (!certifyResp.ok) { let errorMsg = "Unknown error"; try { errorMsg = ((await certifyResp.json()) as { error: string }).error; } catch {} toast({ description: errorMsg, status: "error", title: "Failed to certify game night", }); return; } toast({ description: "Game night certified", status: "success", title: "Success", }); const newEventData = eventData; newEventData[ eventData.findIndex((e) => e.id === eventId) ].reached_minimum_player_count = true; setEventData([...newEventData]); setSelectedEvent(""); } async function markAnswered(eventId: string) { const answerResp = await fetch(`/api/events-team/events/${eventId}/solve`, { body: "{}", headers: { "content-type": "application/json", }, method: "POST", }); closeAnswered(); if (!answerResp.ok) { toast({ description: "Failed to mark as solved", status: "error", title: "Oops", }); return; } const newEventData = eventData; newEventData[eventData.findIndex((e) => e.id === eventId)].answered_at = Date.now(); setEventData([...newEventData]); setSelectedEvent(""); } async function markComplete(eventId: string) { const completeResp = await fetch( `/api/events-team/events/${eventId}/complete`, { body: "{}", headers: { "content-type": "application/json", }, method: "POST", }, ); closeComplete(); if (!completeResp.ok) { let msg = "Unknown error"; try { msg = ((await completeResp.json()) as { error: string }).error; } catch {} toast({ description: msg, status: "error", title: "Failed to complete", }); return; } toast({ description: "Event marked as completed", status: "success", title: "Success", }); const newEventData = eventData; // Technically this won't be the same as the time in the db, but that doesn't matter since this is just to hide the button newEventData[eventData.findIndex((e) => e.id === eventId)].performed_at = Date.now(); setEventData([...newEventData]); setSelectedEvent(""); } async function markForgotten(eventId: string) { const forgottenResp = await fetch( `/api/events-team/events/${eventId}/forgotten`, { body: "{}", headers: { "content-type": "application/json", }, method: "POST", }, ); onForgottenClose(); if (!forgottenResp.ok) { let msg = "Unknown error"; try { msg = ((await forgottenResp.json()) as { error: string }).error; } catch {} toast({ description: msg, status: "error", title: "Failed to forget", }); return; } const newEventData = eventData; newEventData[eventData.findIndex((e) => e.id === eventId)].performed_at = 0; setEventData([...newEventData]); setSelectedEvent(""); } async function deleteEvent(eventId: string) { const deleteResp = await fetch(`/api/events-team/events/${eventId}`, { method: "DELETE", }); onDeleteClose(); if (!deleteResp.ok) { let msg = "Unknown error"; try { msg = ((await deleteResp.json()) as { error: string }).error; } catch {} toast({ description: msg, status: "error", title: "Failed to delete", }); return; } setEventData(eventData.filter((e) => e.id !== eventId)); setSelectedEvent(""); } async function reschedule(eventId: string) { const newDate = ( document.getElementById("reschedule-input") as HTMLInputElement ).value; const day = newDate.split("-").at(2); const rescheduleResp = await fetch(`/api/events-team/events/${eventId}`, { body: JSON.stringify({ day }), headers: { "content-type": "application/json", }, method: "PATCH", }); if (!rescheduleResp.ok) { let msg = "Unknown error"; try { msg = ((await rescheduleResp.json()) as { error: string }).error; } catch {} toast({ description: msg, status: "error", title: "Failed to reschedule", }); return; } const newEventData = eventData; newEventData[eventData.findIndex((e) => e.id === eventId)].day = day; setEventData([...newEventData]); setSelectedEvent(""); onRescheduleClose(); toast({ description: `Event rescheduled to ${newDate}`, status: "success", title: "Rescheduled", }); } return ( Certify Game Night By certifying this game night, you confirm that the minimum number of players was met and you were provided proof. Mark as Completed By marking this event as completed, you confirm that the event creator has performed this event Mark as Solved Are you sure you want to mark this riddle as solved? Mark as Forgotten Are you sure you want to mark this event as forgotten? The creator will be given a 5 point penalty. Delete Event? You are about to permanently delete this event. Are you sure you want to continue? Reschedule Event New date: {eventData .map((event) => { if (!showOld && event.day < new Date().getUTCDate()) return; const eventCreatorName = members.find( (member) => member.id === event.created_by, )?.name; const eventColors: { [k: string]: string } = { fotd: "cyan", gamenight: "blue", rotw: "magenta", qotd: "#9900FF", }; return ( } spacing="4"> Date {event.year}-{event.month}-{event.day} new Date().getUTCDate()) || can_approve ? undefined : "none" } icon={ } ml="8px" onClick={() => { setSelectedEvent(event.id); onRescheduleOpen(); }} /> Event Type {event.type.toUpperCase()} Event Details {event.details} {event.type === "rotw" ? ( Riddle Answer {event.answer} ) : null} Host {eventCreatorName ? `${eventCreatorName} (${event.created_by})` : event.created_by} {can_approve && event.pending ? ( <> ) : null} {can_approve && !event.pending && typeof event.performed_at !== "number" ? ( <> ) : null} {can_approve && !event.pending && event.approved && event.performed_at && event.type === "rotw" && !event.answered_at ? ( ) : null} {can_approve && event.approved && event.type === "gamenight" && event.performed_at && !event.reached_minimum_player_count ? ( ) : null} Status:{" "} {event.pending ? "Pending" : event.approved ? event.performed_at ? "Completed" : "Approved" : "Denied"} new Date().getUTCDate()) || can_approve ? undefined : "none" } icon={ } marginLeft="auto" marginRight={0} onClick={() => { setSelectedEvent(event.id); onDeleteOpen(); }} /> ); }) .filter((e) => e)} Show old events { setShowOld(e.target.checked); setEventData([...eventData]); }} /> Book an Event Historical Events {can_approve ? ( <> Outstanding Events from Last Month Events Team Member Management ) : null} ); }