384 lines
10 KiB
TypeScript
384 lines
10 KiB
TypeScript
import { useLoaderData } from "@remix-run/react";
|
|
import {
|
|
Button,
|
|
Container,
|
|
Heading,
|
|
Input,
|
|
Link,
|
|
Modal,
|
|
ModalBody,
|
|
ModalCloseButton,
|
|
ModalContent,
|
|
ModalFooter,
|
|
ModalHeader,
|
|
ModalOverlay,
|
|
NumberDecrementStepper,
|
|
NumberIncrementStepper,
|
|
NumberInput,
|
|
NumberInputField,
|
|
NumberInputStepper,
|
|
Table,
|
|
TableCaption,
|
|
TableContainer,
|
|
Tbody,
|
|
Td,
|
|
Text,
|
|
Th,
|
|
Thead,
|
|
Tr,
|
|
useDisclosure,
|
|
useToast,
|
|
} from "@chakra-ui/react";
|
|
import { FormEvent, useState } from "react";
|
|
|
|
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 etData = await context.env.D1.prepare(
|
|
"SELECT id, name, points, roblox_id FROM et_members;",
|
|
).all();
|
|
|
|
if (etData.error)
|
|
throw new Response(null, {
|
|
status: 500,
|
|
});
|
|
|
|
return { can_manage: true, members: etData.results } as {
|
|
can_manage: boolean;
|
|
members: { [k: string]: any }[];
|
|
};
|
|
}
|
|
|
|
export default function () {
|
|
const toast = useToast();
|
|
|
|
async function removeMember(id: string) {
|
|
const removeResp = await fetch("/api/events-team/team-members/user", {
|
|
body: JSON.stringify({ id }),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (!removeResp.ok) {
|
|
toast({
|
|
description: "Failed to remove member, try again later",
|
|
status: "error",
|
|
title: "Oops",
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
toast({
|
|
description: "The member was removed from the roster",
|
|
status: "success",
|
|
title: "Member Removed",
|
|
});
|
|
|
|
setMemberData(memberData.filter((member) => member.id !== id));
|
|
}
|
|
|
|
async function addMember() {
|
|
const addResp = await fetch("/api/events-team/team-members/user", {
|
|
body: JSON.stringify({
|
|
id: addingMemberId,
|
|
name: addingMemberName,
|
|
roblox_username: addingMemberRoblox,
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
method: "POST",
|
|
});
|
|
|
|
if (!addResp.ok) {
|
|
toast({
|
|
description: "Failed to add member, try again later",
|
|
status: "error",
|
|
title: "Oops",
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
toast({
|
|
description: `Member ${addingMemberName} was added to the roster`,
|
|
status: "success",
|
|
title: "Member Added",
|
|
});
|
|
|
|
location.reload();
|
|
}
|
|
|
|
const data = useLoaderData<typeof loader>();
|
|
const [realtimePoints, setRealtimePoints] = useState(0);
|
|
const [currentModalMember, setModalMember] = useState("");
|
|
const [currentDelMember, setDelMember] = useState({ id: "", name: "" });
|
|
const [memberData, setMemberData] = useState(data.members);
|
|
const [addingMemberId, setAddingMemberId] = useState("");
|
|
const [addingMemberName, setAddingMemberName] = useState("");
|
|
const [addingMemberRoblox, setAddingMemberRoblox] = useState("");
|
|
const { isOpen, onClose, onOpen } = useDisclosure();
|
|
const {
|
|
isOpen: isDelConfirmOpen,
|
|
onClose: closeDelConfirm,
|
|
onOpen: openDelConfirm,
|
|
} = useDisclosure();
|
|
const {
|
|
isOpen: isAddMemberOpen,
|
|
onClose: closeAddMember,
|
|
onOpen: openAddMember,
|
|
} = useDisclosure();
|
|
const isManagement = data.can_manage;
|
|
|
|
async function updatePoints(id: string, points: number) {
|
|
const updateResp = await fetch(`/api/events-team/points/${id}`, {
|
|
body: JSON.stringify({ points }),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
method: "POST",
|
|
});
|
|
|
|
if (!updateResp.ok) {
|
|
toast({
|
|
description: "Failed to update points",
|
|
status: "error",
|
|
title: "Oops!",
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
toast({
|
|
description: `Point count changed to ${points}`,
|
|
status: "success",
|
|
title: "Points updated",
|
|
});
|
|
|
|
const newMemberData = memberData;
|
|
newMemberData[memberData.findIndex((m) => m.id === id)].points = points;
|
|
|
|
setMemberData(newMemberData);
|
|
onClose();
|
|
}
|
|
|
|
return (
|
|
<Container maxW="container.lg">
|
|
<Modal
|
|
isOpen={isOpen}
|
|
onClose={() => {
|
|
setRealtimePoints(0);
|
|
onClose();
|
|
}}
|
|
>
|
|
<ModalOverlay />
|
|
<ModalContent>
|
|
<ModalHeader>Modify Points</ModalHeader>
|
|
<ModalCloseButton />
|
|
<ModalBody>
|
|
<NumberInput
|
|
allowMouseWheel
|
|
defaultValue={realtimePoints}
|
|
onChange={(n) => setRealtimePoints(parseInt(n))}
|
|
mt="8px"
|
|
>
|
|
<NumberInputField />
|
|
<NumberInputStepper>
|
|
<NumberIncrementStepper />
|
|
<NumberDecrementStepper />
|
|
</NumberInputStepper>
|
|
</NumberInput>
|
|
</ModalBody>
|
|
{isManagement ? (
|
|
<ModalFooter>
|
|
<Button
|
|
onClick={() => {
|
|
setRealtimePoints(0);
|
|
onClose();
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
colorScheme="blue"
|
|
onClick={async () =>
|
|
await updatePoints(currentModalMember, realtimePoints)
|
|
}
|
|
>
|
|
Update Points
|
|
</Button>
|
|
</ModalFooter>
|
|
) : null}
|
|
</ModalContent>
|
|
</Modal>
|
|
<Modal isOpen={isDelConfirmOpen} onClose={closeDelConfirm}>
|
|
<ModalOverlay />
|
|
<ModalContent>
|
|
<ModalHeader>Remove Member</ModalHeader>
|
|
<ModalCloseButton />
|
|
<ModalBody>
|
|
<Text>
|
|
You are about to remove {currentDelMember.name} from the Events
|
|
Team roster, this will clear all of their data. Are you sure you
|
|
want to do this?
|
|
</Text>
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button
|
|
colorScheme="blue"
|
|
onClick={() => {
|
|
setDelMember({ id: "", name: "" });
|
|
closeDelConfirm();
|
|
}}
|
|
>
|
|
No
|
|
</Button>
|
|
<Button
|
|
colorScheme="red"
|
|
onClick={async () => {
|
|
await removeMember(currentDelMember.id);
|
|
setDelMember({ id: "", name: "" });
|
|
closeDelConfirm();
|
|
}}
|
|
ml="8px"
|
|
>
|
|
Yes, Remove
|
|
</Button>
|
|
</ModalFooter>
|
|
</ModalContent>
|
|
</Modal>
|
|
<Modal isOpen={isAddMemberOpen} onClose={closeAddMember}>
|
|
<ModalOverlay />
|
|
<ModalContent>
|
|
<ModalHeader>Add Member</ModalHeader>
|
|
<ModalCloseButton />
|
|
<ModalBody>
|
|
<Heading size="xs">User ID</Heading>
|
|
<Input
|
|
maxLength={19}
|
|
onBeforeInput={(e) => {
|
|
const {
|
|
data,
|
|
}: { data?: string } & FormEvent<HTMLInputElement> = e;
|
|
|
|
if (data?.match(/\D/)) e.preventDefault();
|
|
}}
|
|
onChange={(e) => setAddingMemberId(e.target.value)}
|
|
mb="16px"
|
|
type="number"
|
|
/>
|
|
<Heading size="xs">Name</Heading>
|
|
<Input
|
|
maxLength={64}
|
|
onChange={(e) => setAddingMemberName(e.target.value)}
|
|
mb="16px"
|
|
/>
|
|
<Heading size="xs">Roblox Username (optional)</Heading>
|
|
<Input
|
|
maxLength={20}
|
|
onBeforeInput={(e) => {
|
|
const {
|
|
data,
|
|
}: { data?: string } & FormEvent<HTMLInputElement> = e;
|
|
|
|
if (!data) return;
|
|
|
|
if (
|
|
data.match(/\W/) ||
|
|
data.length > 20 ||
|
|
(data.match(/_/g)?.length || 0) > 1 ||
|
|
data.startsWith("_")
|
|
)
|
|
e.preventDefault();
|
|
}}
|
|
onChange={(e) => setAddingMemberRoblox(e.target.value)}
|
|
/>
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button onClick={closeAddMember}>Close</Button>
|
|
<Button
|
|
colorScheme="blue"
|
|
onClick={async () => await addMember()}
|
|
ml="8px"
|
|
>
|
|
Add
|
|
</Button>
|
|
</ModalFooter>
|
|
</ModalContent>
|
|
</Modal>
|
|
<Heading>Events Team Members</Heading>
|
|
<TableContainer mt="16px">
|
|
<Table variant="simple">
|
|
<TableCaption>
|
|
Points are updated at the end of the month
|
|
</TableCaption>
|
|
<Thead>
|
|
<Tr>
|
|
<Th>Discord ID</Th>
|
|
<Th>Name</Th>
|
|
<Th>Roblox ID</Th>
|
|
<Th>Points</Th>
|
|
<Th>Remove</Th>
|
|
</Tr>
|
|
</Thead>
|
|
<Tbody>
|
|
{memberData.map((member) => (
|
|
<Tr>
|
|
<Td>{member.id}</Td>
|
|
<Td>{member.name}</Td>
|
|
<Td>{member.roblox_id}</Td>
|
|
<Td>
|
|
{isManagement ? (
|
|
<Link
|
|
onClick={() => {
|
|
setModalMember(member.id);
|
|
onOpen();
|
|
}}
|
|
>
|
|
{member.points}
|
|
</Link>
|
|
) : (
|
|
member.points
|
|
)}
|
|
</Td>
|
|
<Td>
|
|
{isManagement ? (
|
|
<Link
|
|
onClick={() => {
|
|
setDelMember({ id: member.id, name: member.name });
|
|
openDelConfirm();
|
|
}}
|
|
>
|
|
Remove
|
|
</Link>
|
|
) : null}
|
|
</Td>
|
|
</Tr>
|
|
))}
|
|
</Tbody>
|
|
</Table>
|
|
</TableContainer>
|
|
{isManagement ? (
|
|
<Link color="#646cff" onClick={openAddMember} mt="16px">
|
|
Add Member
|
|
</Link>
|
|
) : null}
|
|
</Container>
|
|
);
|
|
}
|