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
597 lines (549 sloc) 16.6 KB
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 { type FormEvent, useState } from "react";
export async function loader({ context }: { context: RequestContext }) {
if (!context.data.current_user)
throw new Response(null, {
status: 401,
});
if (![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,
});
const now = new Date();
const members = etData.results as { [k: string]: any }[];
const currentMonthEvents = await context.env.D1.prepare(
"SELECT answered_at, created_by, performed_at, reached_minimum_player_count, type FROM events WHERE year = ? AND month = ?;",
)
.bind(now.getUTCFullYear(), now.getUTCMonth() + 1)
.all();
if (!currentMonthEvents.error) {
for (const event of currentMonthEvents.results as { [k: string]: any }[]) {
const memberIdx = members.findIndex((m) => m.id === event.created_by);
if (memberIdx === -1) continue;
if (event.performed_at) members[memberIdx].points += 10;
if (event.type === "gamenight" && event.reached_minimum_player_count)
members[memberIdx].points += 10;
if (
event.type === "rotw" &&
event.answered_at - event.performed_at >= 86400000
)
members[memberIdx].points += 10;
if (!event.performed_at && event.day < now.getUTCDate())
members[memberIdx].points -= 5;
}
}
return { members } as {
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 {
isOpen: isNameChangeOpen,
onClose: closeNameChange,
onOpen: openNameChange,
} = useDisclosure();
const {
isOpen: isChangeRobloxOpen,
onClose: closeChangeRoblox,
onOpen: openChangeRoblox,
} = useDisclosure();
function validateRobloxName(e: FormEvent<HTMLInputElement>) {
const data = (e.target as HTMLInputElement).value as string;
if (!data) return;
if (
data.match(/\W/) ||
data.length > 20 ||
// Need Number pseudo-constructor since matches might be null
(data.match(/_/g)?.length || 0) > 1 ||
data.startsWith("_")
)
e.preventDefault();
}
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={isChangeRobloxOpen} onClose={closeChangeRoblox}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Change Roblox User</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Heading mb="8px" size="xs">
New Roblox Username
</Heading>
<Input
maxLength={20}
onBeforeInput={validateRobloxName}
onChange={(e) => setAddingMemberRoblox(e.target.value)}
placeholder="builderman"
/>
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
setAddingMemberRoblox("");
closeChangeRoblox();
}}
>
Cancel
</Button>
<Button
colorScheme="blue"
ml="8px"
onClick={async () => {
const changeResp = await fetch(
"/api/events-team/team-members/user",
{
body: JSON.stringify({
id: currentModalMember,
roblox_username: addingMemberRoblox,
}),
headers: {
"content-type": "application/json",
},
method: "PATCH",
},
);
if (!changeResp.ok) {
let errorMsg = "Unknown error";
try {
errorMsg = ((await changeResp.json()) as { error: string })
.error;
} catch {}
toast({
description: errorMsg,
status: "error",
title: "Failed to change",
});
return;
}
toast({
description: "Roblox information updated",
status: "success",
title: "Change successful",
});
const newMemberData = memberData;
newMemberData[
memberData.findIndex((m) => m.id === currentModalMember)
].roblox_id = (
(await changeResp.json()) as {
name: string;
roblox_id: number;
}
).roblox_id;
setMemberData([...newMemberData]);
closeChangeRoblox();
setModalMember("");
setAddingMemberRoblox("");
}}
>
Change
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<Modal isOpen={isNameChangeOpen} onClose={closeNameChange}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Change Name</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Input
maxLength={64}
onChange={(e) => setAddingMemberName(e.target.value)}
placeholder="New name"
/>
</ModalBody>
<ModalFooter>
<Button
onClick={() => {
setAddingMemberName("");
closeNameChange();
}}
>
Cancel
</Button>
<Button
colorScheme="blue"
ml="8px"
onClick={async () => {
const nameUpdateResp = await fetch(
"/api/events-team/team-members/user",
{
body: JSON.stringify({
id: currentModalMember,
name: addingMemberName,
}),
headers: {
"content-type": "application/json",
},
method: "PATCH",
},
);
const newName = addingMemberName;
closeNameChange();
setAddingMemberName("");
if (!nameUpdateResp.ok) {
let errorMsg = "Unknown error";
try {
errorMsg = (
(await nameUpdateResp.json()) as { error: string }
).error;
} catch {}
toast({
description: errorMsg,
status: "error",
title: "Error",
});
return;
}
toast({
description: `Name changed to ${newName}`,
status: "success",
title: "Name changed",
});
const newMemberData = memberData;
newMemberData[
memberData.findIndex((m) => m.id === currentModalMember)
].name = newName;
setMemberData([...newMemberData]);
}}
>
Update Name
</Button>
</ModalFooter>
</ModalContent>
</Modal>
<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>
<ModalFooter>
<Button
onClick={() => {
setRealtimePoints(0);
onClose();
}}
>
Cancel
</Button>
<Button
colorScheme="blue"
ml="8px"
onClick={async () =>
await updatePoints(currentModalMember, realtimePoints)
}
>
Update Points
</Button>
</ModalFooter>
</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={validateRobloxName}
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>
Click/tap on a user's points count to change their points, their
user id to see and manage strikes.
</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>
<Link href={`/et-members/strikes/${member.id}`}>
{member.id}
</Link>
</Td>
<Td>
<Link
onClick={() => {
setModalMember(member.id);
openNameChange();
}}
>
{member.name}
</Link>
</Td>
<Td>
<Link
onClick={() => {
setModalMember(member.id);
openChangeRoblox();
}}
>
{member.roblox_id}
</Link>
</Td>
<Td>
<Link
onClick={() => {
setModalMember(member.id);
onOpen();
}}
>
{member.points}
</Link>
</Td>
<Td>
<Link
onClick={() => {
setDelMember({ id: member.id, name: member.name });
openDelConfirm();
}}
>
Remove
</Link>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Link color="#646cff" onClick={openAddMember} mt="16px">
Add Member
</Link>
</Container>
);
}