Skip to content
Permalink
84e7df6c30
Switch branches/tags

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?
Go to file
 
 
Cannot retrieve contributors at this time
721 lines (657 sloc) 21.1 KB
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<typeof loader>();
const [eventData, setEventData] = useState(events);
const { isOpen, onClose, onOpen } = useDisclosure();
const {
isOpen: isCompleteOpen,
onClose: closeComplete,
onOpen: openComplete,
} = 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 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 (
<Container maxW="container.lg">
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Certify Game Night</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text>
By certifying this game night, you confirm that the minimum number
of players was met and you were provided proof.
</Text>
</ModalBody>
<ModalFooter>
<Button colorScheme="red" onClick={onClose}>
Cancel
</Button>
<Button
colorScheme="blue"
ml="8px"
onClick={async () => {
await certify(selectedEvent);
onClose();
}}
>
Certify
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Modal isOpen={isCompleteOpen} onClose={closeComplete}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Mark as Completed</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text>
By marking this event as completed, you confirm that the event
creator has performed this event
</Text>
</ModalBody>
<ModalFooter>
<Button onClick={closeComplete}>Cancel</Button>
<Button
colorScheme="blue"
ml="8px"
onClick={async () => await markComplete(selectedEvent)}
>
Mark as Complete
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Modal isOpen={isForgottenOpen} onClose={onForgottenClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Mark as Forgotten</ModalHeader>
<ModalCloseButton />
<ModalBody>
Are you sure you want to mark this event as forgotten? The creator
will be given a 5 point penalty.
</ModalBody>
<ModalFooter>
<Button onClick={onForgottenClose}>Cancel</Button>
<Button
colorScheme="blue"
ml="8px"
onClick={async () => await markForgotten(selectedEvent)}
>
Mark Forgotten
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Modal isOpen={isDeleteOpen} onClose={onDeleteClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Delete Event?</ModalHeader>
<ModalCloseButton />
<ModalBody>
You are about to permanently delete this event. Are you sure you
want to continue?
</ModalBody>
<ModalFooter>
<Button onClick={onDeleteClose}>Cancel</Button>
<Button
colorScheme="red"
ml="8px"
onClick={async () => await deleteEvent(selectedEvent)}
>
Delete
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Modal isOpen={isRescheduleOpen} onClose={onRescheduleClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Reschedule Event</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text>
New date:
<input
id="reschedule-input"
type="date"
style={{ marginLeft: "8px" }}
min={new Date().toISOString().split("T").at(0)}
max={(function () {
const date = new Date();
date.setUTCMonth(date.getUTCMonth() + 1, 0);
return date.toISOString().split("T").at(0);
})()}
/>
</Text>
</ModalBody>
<ModalFooter>
<Button onClick={onRescheduleClose}>Cancel</Button>
<Button
colorScheme="blue"
ml="8px"
onClick={async () => await reschedule(selectedEvent)}
>
Reschedule
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<VStack spacing="8">
{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 (
<Card
borderColor={eventColors[event.type]}
borderWidth="2px"
w="100%"
>
<CardBody>
<Stack divider={<StackDivider />} spacing="4">
<Box>
<Heading size="sm">Date</Heading>
<Text fontSize="sm" pt="2">
{event.year}-{event.month}-{event.day}
<IconButton
aria-label="Edit event date"
display={
(event.created_by === user_id &&
event.day > new Date().getUTCDate()) ||
can_approve
? undefined
: "none"
}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z" />
<path
fill-rule="evenodd"
d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5z"
/>
</svg>
}
ml="8px"
onClick={() => {
setSelectedEvent(event.id);
onRescheduleOpen();
}}
/>
</Text>
</Box>
<Box>
<Heading size="sm">Event Type</Heading>
<Text fontSize="sm" pt="2">
{event.type.toUpperCase()}
</Text>
</Box>
<Box>
<Heading size="sm">Event Details</Heading>
<Text fontSize="sm" pt="2">
{event.details}
</Text>
</Box>
{event.type === "rotw" ? (
<Box>
<Heading size="sm">Riddle Answer</Heading>
<Text fontSize="sm" pt="2">
{event.answer}
</Text>
</Box>
) : null}
<Box>
<Heading size="sm">Host</Heading>
<Text fontSize="sm" pt="2">
{eventCreatorName
? `${eventCreatorName} (${event.created_by})`
: event.created_by}
</Text>
</Box>
</Stack>
</CardBody>
<CardFooter>
<Flex gap="8px" mr="8px">
{can_approve && event.pending ? (
<>
<Button
colorScheme="red"
onClick={async () => await decide(false, event.id)}
>
Reject
</Button>
<Button
colorScheme="blue"
onClick={async () => await decide(true, event.id)}
>
Approve
</Button>
</>
) : null}
{can_approve &&
!event.pending &&
typeof event.performed_at !== "number" ? (
<>
<Button
colorScheme="blue"
onClick={() => {
setSelectedEvent(event.id);
openComplete();
}}
>
Mark as Completed
</Button>
<Button
colorScheme="red"
onClick={() => {
setSelectedEvent(event.id);
onForgottenOpen();
}}
>
Mark as Forgotten
</Button>
</>
) : null}
{can_approve &&
event.approved &&
event.type === "gamenight" &&
event.performed_at &&
!event.reached_minimum_player_count ? (
<Button
colorScheme="blue"
onClick={() => {
setSelectedEvent(event.id);
onOpen();
}}
>
Certify Game Night
</Button>
) : null}
</Flex>
<Text alignSelf="center" fontSize="sm">
Status:{" "}
{event.pending
? "Pending"
: event.approved
? event.performed_at
? "Completed"
: "Approved"
: "Denied"}
</Text>
<IconButton
alignSelf="center"
aria-label="Delete event"
display={
(event.created_by === user_id &&
event.day > new Date().getUTCDate()) ||
can_approve
? undefined
: "none"
}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5M8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5m3 .5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 1 0" />
</svg>
}
marginLeft="auto"
marginRight={0}
onClick={() => {
setSelectedEvent(event.id);
onDeleteOpen();
}}
/>
</CardFooter>
</Card>
);
})
.filter((e) => e)}
</VStack>
<VStack alignItems="start" gap="8px" my="16px">
<FormControl>
<FormLabel htmlFor="show-old-events" mb="0">
Show old events
</FormLabel>
<Switch
id="show-old-events"
onChange={(e) => {
setShowOld(e.target.checked);
setEventData([...eventData]);
}}
/>
</FormControl>
<Link color="#646cff" href="/book-event" mt="16px" target="_blank">
Book an Event
</Link>
<Link
color="#646cff"
href="/events-team/historical"
mt="8px"
target="_blank"
>
Historical Events
</Link>
{can_approve ? (
<>
<Link color="#646cff" href="/events-team/outstanding" mt="8px">
Outstanding Events from Last Month
</Link>
<Link color="#646cff" href="/et-members" mb="32px" mt="8px">
Events Team Member Management
</Link>
</>
) : null}
</VStack>
</Container>
);
}