Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add name/roblox change support
  • Loading branch information
regalijan committed Mar 6, 2024
1 parent 5fe2318 commit 2f8ff3b
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 76 deletions.
254 changes: 179 additions & 75 deletions app/routes/et-members.tsx
Expand Up @@ -78,8 +78,7 @@ export async function loader({ context }: { context: RequestContext }) {
}
}

return { can_manage: true, members } as {
can_manage: boolean;
return { members } as {
members: { [k: string]: any }[];
};
}
Expand Down Expand Up @@ -171,7 +170,26 @@ export default function () {
onClose: closeNameChange,
onOpen: openNameChange,
} = 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) {
const updateResp = await fetch(`/api/events-team/points/${id}`, {
Expand Down Expand Up @@ -207,6 +225,93 @@ export default function () {

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>
Expand Down Expand Up @@ -253,8 +358,16 @@ export default function () {
setAddingMemberName("");

if (!nameUpdateResp.ok) {
let errorMsg = "Unknown error";

try {
errorMsg = (
(await nameUpdateResp.json()) as { error: string }
).error;
} catch {}

toast({
description: "Failed to update name, try again later.",
description: errorMsg,
status: "error",
title: "Error",
});
Expand Down Expand Up @@ -298,27 +411,25 @@ export default function () {
</NumberInputStepper>
</NumberInput>
</ModalBody>
{isManagement ? (
<ModalFooter>
<Button
onClick={() => {
setRealtimePoints(0);
onClose();
}}
>
Cancel
</Button>
<Button
colorScheme="blue"
ml="8px"
onClick={async () =>
await updatePoints(currentModalMember, realtimePoints)
}
>
Update Points
</Button>
</ModalFooter>
) : null}
<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}>
Expand Down Expand Up @@ -386,20 +497,7 @@ export default function () {
<Heading size="xs">Roblox Username (optional)</Heading>
<Input
maxLength={20}
onBeforeInput={(e) => {
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();
}}
onBeforeInput={validateRobloxName}
onChange={(e) => setAddingMemberRoblox(e.target.value)}
/>
</ModalBody>
Expand Down Expand Up @@ -435,52 +533,58 @@ export default function () {
{memberData.map((member) => (
<Tr>
<Td>
{isManagement ? (
<Link href={`/et-members/strikes/${member.id}`}>
{member.id}
</Link>
) : (
member.id
)}
<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>{member.name}</Td>
<Td>{member.roblox_id}</Td>
<Td>
{isManagement ? (
<Link
onClick={() => {
setModalMember(member.id);
onOpen();
}}
>
{member.points}
</Link>
) : (
member.points
)}
<Link
onClick={() => {
setModalMember(member.id);
onOpen();
}}
>
{member.points}
</Link>
</Td>
<Td>
{isManagement ? (
<Link
onClick={() => {
setDelMember({ id: member.id, name: member.name });
openDelConfirm();
}}
>
Remove
</Link>
) : null}
<Link
onClick={() => {
setDelMember({ id: member.id, name: member.name });
openDelConfirm();
}}
>
Remove
</Link>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
{isManagement ? (
<Link color="#646cff" onClick={openAddMember} mt="16px">
Add Member
</Link>
) : null}
<Link color="#646cff" onClick={openAddMember} mt="16px">
Add Member
</Link>
</Container>
);
}
58 changes: 57 additions & 1 deletion functions/api/events-team/team-members/user.ts
@@ -1,4 +1,4 @@
import { jsonError } from "../../../common.js";
import { jsonError, jsonResponse } from "../../../common.js";

export async function onRequestDelete(context: RequestContext) {
let body: { id?: string } = {};
Expand Down Expand Up @@ -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) {
const { id, name, roblox_username } = context.data.body;

Expand Down

0 comments on commit 2f8ff3b

Please sign in to comment.