Add name/roblox change support

This commit is contained in:
Regalijan 2024-03-05 23:37:51 -05:00
parent 5fe23184c9
commit 2f8ff3b3cc
Signed by: regalijan
GPG Key ID: 5D4196DA269EF520
2 changed files with 238 additions and 78 deletions

View File

@ -78,8 +78,7 @@ export async function loader({ context }: { context: RequestContext }) {
} }
} }
return { can_manage: true, members } as { return { members } as {
can_manage: boolean;
members: { [k: string]: any }[]; members: { [k: string]: any }[];
}; };
} }
@ -171,7 +170,26 @@ export default function () {
onClose: closeNameChange, onClose: closeNameChange,
onOpen: openNameChange, onOpen: openNameChange,
} = useDisclosure(); } = useDisclosure();
const isManagement = data.can_manage; 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) { async function updatePoints(id: string, points: number) {
const updateResp = await fetch(`/api/events-team/points/${id}`, { const updateResp = await fetch(`/api/events-team/points/${id}`, {
@ -207,6 +225,93 @@ export default function () {
return ( return (
<Container maxW="container.lg"> <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}> <Modal isOpen={isNameChangeOpen} onClose={closeNameChange}>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
@ -253,8 +358,16 @@ export default function () {
setAddingMemberName(""); setAddingMemberName("");
if (!nameUpdateResp.ok) { if (!nameUpdateResp.ok) {
let errorMsg = "Unknown error";
try {
errorMsg = (
(await nameUpdateResp.json()) as { error: string }
).error;
} catch {}
toast({ toast({
description: "Failed to update name, try again later.", description: errorMsg,
status: "error", status: "error",
title: "Error", title: "Error",
}); });
@ -298,7 +411,6 @@ export default function () {
</NumberInputStepper> </NumberInputStepper>
</NumberInput> </NumberInput>
</ModalBody> </ModalBody>
{isManagement ? (
<ModalFooter> <ModalFooter>
<Button <Button
onClick={() => { onClick={() => {
@ -318,7 +430,6 @@ export default function () {
Update Points Update Points
</Button> </Button>
</ModalFooter> </ModalFooter>
) : null}
</ModalContent> </ModalContent>
</Modal> </Modal>
<Modal isOpen={isDelConfirmOpen} onClose={closeDelConfirm}> <Modal isOpen={isDelConfirmOpen} onClose={closeDelConfirm}>
@ -386,20 +497,7 @@ export default function () {
<Heading size="xs">Roblox Username (optional)</Heading> <Heading size="xs">Roblox Username (optional)</Heading>
<Input <Input
maxLength={20} maxLength={20}
onBeforeInput={(e) => { onBeforeInput={validateRobloxName}
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();
}}
onChange={(e) => setAddingMemberRoblox(e.target.value)} onChange={(e) => setAddingMemberRoblox(e.target.value)}
/> />
</ModalBody> </ModalBody>
@ -435,18 +533,31 @@ export default function () {
{memberData.map((member) => ( {memberData.map((member) => (
<Tr> <Tr>
<Td> <Td>
{isManagement ? (
<Link href={`/et-members/strikes/${member.id}`}> <Link href={`/et-members/strikes/${member.id}`}>
{member.id} {member.id}
</Link> </Link>
) : (
member.id
)}
</Td> </Td>
<Td>{member.name}</Td>
<Td>{member.roblox_id}</Td>
<Td> <Td>
{isManagement ? ( <Link
onClick={() => {
setModalMember(member.id);
openNameChange();
}}
>
{member.name}
</Link>
</Td>
<Td>
<Link
onClick={() => {
setModalMember(member.id);
openChangeRoblox();
}}
>
{member.roblox_id}
</Link>
</Td>
<Td>
<Link <Link
onClick={() => { onClick={() => {
setModalMember(member.id); setModalMember(member.id);
@ -455,12 +566,8 @@ export default function () {
> >
{member.points} {member.points}
</Link> </Link>
) : (
member.points
)}
</Td> </Td>
<Td> <Td>
{isManagement ? (
<Link <Link
onClick={() => { onClick={() => {
setDelMember({ id: member.id, name: member.name }); setDelMember({ id: member.id, name: member.name });
@ -469,18 +576,15 @@ export default function () {
> >
Remove Remove
</Link> </Link>
) : null}
</Td> </Td>
</Tr> </Tr>
))} ))}
</Tbody> </Tbody>
</Table> </Table>
</TableContainer> </TableContainer>
{isManagement ? (
<Link color="#646cff" onClick={openAddMember} mt="16px"> <Link color="#646cff" onClick={openAddMember} mt="16px">
Add Member Add Member
</Link> </Link>
) : null}
</Container> </Container>
); );
} }

View File

@ -1,4 +1,4 @@
import { jsonError } from "../../../common.js"; import { jsonError, jsonResponse } from "../../../common.js";
export async function onRequestDelete(context: RequestContext) { export async function onRequestDelete(context: RequestContext) {
let body: { id?: string } = {}; let body: { id?: string } = {};
@ -28,6 +28,62 @@ export async function onRequestDelete(context: RequestContext) {
}); });
} }
export async function onRequestPatch(context: RequestContext) {
let body: { id?: string; name?: string; roblox_username?: string } = {};
try {
body = await context.request.json();
} catch {
return jsonError("Invalid body", 400);
}
if (typeof body.name !== "string" && typeof body.roblox_username !== "string")
return jsonError("At least one property must be provided", 400);
const updates = [];
if (body.name?.length) updates.push({ query: "name = ?", value: body.name });
if (typeof body.roblox_username === "string" && body.roblox_username) {
const robloxResolveResp = await fetch(
"https://users.roblox.com/v1/usernames/users",
{
body: JSON.stringify({
excludeBannedUsers: true,
usernames: [body.roblox_username],
}),
headers: {
"content-type": "application/json",
},
method: "POST",
},
);
const { data } = (await robloxResolveResp.json()) as {
data: { [k: string]: any }[];
};
if (!data.length)
return jsonError("No Roblox user exists with that name", 400);
updates.push({ query: "roblox_id = ?", value: data[0].id });
}
await context.env.D1.prepare(
`UPDATE et_members
SET (${updates.join(", ")});`,
)
.bind(...updates.map((u) => u.value))
.run();
return jsonResponse(
JSON.stringify({
name: body.name,
roblox_id: updates.find((u) => typeof u.value === "number")?.value,
}),
);
}
export async function onRequestPost(context: RequestContext) { export async function onRequestPost(context: RequestContext) {
const { id, name, roblox_username } = context.data.body; const { id, name, roblox_username } = context.data.body;