Permalink
Cannot retrieve contributors at this time
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?
car-crushers-portal/app/routes/et-members.tsx
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
396 lines (369 sloc)
10.5 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
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> | |
{isManagement ? ( | |
<Link | |
onClick={() => | |
location.assign(`/et-members/strikes/${member.id}`) | |
} | |
> | |
{member.id} | |
</Link> | |
) : ( | |
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> | |
); | |
} |