diff --git a/app/routes/et-members.tsx b/app/routes/et-members.tsx index 5aa6713..69f0f0d 100644 --- a/app/routes/et-members.tsx +++ b/app/routes/et-members.tsx @@ -325,7 +325,8 @@ export default function () { - Points are updated at the end of the month + Click/tap on a user's points count to change their points, their + user id to see and manage strikes. @@ -339,7 +340,19 @@ export default function () { {memberData.map((member) => ( - +
{member.id} + {isManagement ? ( + + location.assign(`/et-members/strikes/${member.id}`) + } + > + {member.id} + + ) : ( + member.id + )} + {member.name} {member.roblox_id} diff --git a/app/routes/et-members_.strikes_.$uid.tsx b/app/routes/et-members_.strikes_.$uid.tsx new file mode 100644 index 0000000..88bad4c --- /dev/null +++ b/app/routes/et-members_.strikes_.$uid.tsx @@ -0,0 +1,240 @@ +import { + Button, + Container, + Heading, + Link, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Table, + TableContainer, + Tbody, + Td, + Text, + Textarea, + Th, + Thead, + Tr, + useDisclosure, + useToast, +} from "@chakra-ui/react"; +import { LoaderFunctionArgs } from "@remix-run/cloudflare"; +import { useLoaderData } from "@remix-run/react"; +import { useState } from "react"; + +export async function loader({ + context, + params, +}: { + context: RequestContext; + params: LoaderFunctionArgs & { uid: string }; +}) { + const { current_user: user } = context.data; + + if (!user) + throw new Response(null, { + status: 401, + }); + + if (![1 << 3, 1 << 4, 1 << 12].find((p) => user.permissions & p)) + throw new Response(null, { + status: 403, + }); + + const strikeData = await context.env.D1.prepare( + "SELECT * FROM et_strikes WHERE user = ?;", + ) + .bind(params.uid) + .all(); + + return { + can_manage: Boolean([1 << 4, 1 << 12].find((p) => user.permissions & p)), + strikes: strikeData.results, + user: params.uid, + }; +} + +export default function () { + const { can_manage, strikes, user } = useLoaderData(); + const [strikeData, setStrikeData] = useState(strikes); + const toast = useToast(); + const [rmStrikeId, setRmStrikeId] = useState(""); + const [strikeReason, setStrikeReason] = useState(""); + + async function removeStrike(id: string) { + const removeResp = await fetch(`/api/events-team/strikes/${id}`, { + method: "DELETE", + }); + + if (!removeResp.ok) { + let msg = "Unknown error"; + + try { + msg = ((await removeResp.json()) as { error: string }).error; + } catch {} + + toast({ + description: msg, + status: "error", + title: "Failed to remove strike", + }); + + return; + } + + toast({ + description: `Strike ${id} was removed`, + status: "success", + title: "Strike Removed", + }); + + setStrikeData(strikeData.filter((strike) => strike.id !== id)); + closeRmStrike(); + } + + async function addStrike() { + const addStrikeResp = await fetch("/api/events-team/strikes/new", { + body: JSON.stringify({ + reason: strikeReason, + user, + }), + headers: { + "content-type": "application/json", + }, + method: "POST", + }); + + if (!addStrikeResp.ok) { + let msg = "Unknown error"; + + try { + msg = ((await addStrikeResp.json()) as { error: string }).error; + } catch {} + + toast({ + description: msg, + status: "error", + title: "Failed to add strike", + }); + + return; + } + + toast({ + description: "Strike added", + status: "success", + title: "Success", + }); + + const newStrikeData = strikeData; + + newStrikeData.push(await addStrikeResp.json()); + setStrikeData(newStrikeData); + closeAddStrike(); + } + + const { + isOpen: rmStrikeOpen, + onClose: closeRmStrike, + onOpen: openRmStrike, + } = useDisclosure(); + const { + isOpen: addStrikeOpen, + onClose: closeAddStrike, + onOpen: openAddStrike, + } = useDisclosure(); + + return ( + + + + + Remove Strike + + + Are you sure you want to remove this strike? + + + + + + + + + + + Add Strike + + + + Reason + +