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/me.tsx
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
443 lines (412 sloc)
13.1 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 { | |
Button, | |
Container, | |
Divider, | |
Heading, | |
Link, | |
ListItem, | |
Modal, | |
ModalBody, | |
ModalCloseButton, | |
ModalContent, | |
ModalHeader, | |
ModalOverlay, | |
Stack, | |
Table, | |
TableCaption, | |
TableContainer, | |
Tbody, | |
Td, | |
Text, | |
Th, | |
Thead, | |
Tr, | |
UnorderedList, | |
useDisclosure, | |
useToast, | |
} from "@chakra-ui/react"; | |
import { type Dispatch, type SetStateAction, useEffect, useState } from "react"; | |
import { useLoaderData } from "@remix-run/react"; | |
export async function loader({ context }: { context: RequestContext }) { | |
const { current_user: currentUser } = context.data; | |
if (!currentUser) throw new Response(null, { status: 401 }); | |
const d1Promises = []; | |
for (const itemType of ["appeals", "inactivity_notices", "reports"]) | |
d1Promises.push( | |
context.env.D1.prepare( | |
`SELECT * | |
FROM ${itemType} | |
WHERE json_extract(user, '$.id') = ? | |
ORDER BY created_at DESC;`, | |
) | |
.bind(currentUser.id) | |
.all(), | |
); | |
const settledPromises = await Promise.allSettled(d1Promises); | |
let etData: { [k: string]: any } | null = null; | |
if (currentUser.permissions & (1 << 3)) { | |
etData = await context.env.D1.prepare( | |
"SELECT name, points, roblox_id FROM et_members WHERE id = ?;", | |
) | |
.bind(currentUser.id) | |
.first(); | |
} | |
return { | |
etData, | |
items: settledPromises.map((p) => { | |
if (p.status === "fulfilled") return p.value.results; | |
return null; | |
}) as any as ({ [k: string]: any }[] | null)[], | |
permissions: currentUser.permissions as number, | |
}; | |
} | |
export default function () { | |
const data: { | |
etData: { [k: string]: any } | null; | |
items: ({ [k: string]: any }[] | null)[]; | |
permissions: number; | |
} = useLoaderData<typeof loader>(); | |
const timeStates: { | |
[k: number]: { data: string; set: Dispatch<SetStateAction<string>> }; | |
} = {}; | |
const toast = useToast(); | |
for (const result of data.items) { | |
if (!result) continue; | |
for (const row of result) { | |
const [data, set] = useState(new Date(row.created_at).toUTCString()); | |
timeStates[row.created_at] = { | |
data, | |
set, | |
}; | |
useEffect(() => { | |
timeStates[row.created_at].set( | |
new Date(row.created_at).toLocaleString(), | |
); | |
}, [row.created_at]); | |
} | |
} | |
async function fetchItem(id: string, type: string) { | |
const itemResp = await fetch(`/api/me/items/${type}/${id}`); | |
if (!itemResp.ok) { | |
let error: string; | |
try { | |
error = ((await itemResp.json()) as { error: string }).error; | |
} catch { | |
error = "Unknown error"; | |
} | |
toast({ | |
description: error, | |
isClosable: true, | |
status: "error", | |
title: "Oops", | |
}); | |
return; | |
} | |
const data: { [k: string]: any } = await itemResp.json(); | |
switch (type) { | |
case "appeal": | |
setModalBody( | |
<ModalContent> | |
<ModalHeader>View Appeal</ModalHeader> | |
<ModalCloseButton /> | |
<ModalBody> | |
<Heading size="lg">Why were you banned?</Heading> | |
<br /> | |
<Text> | |
<i>{data.ban_reason}</i> | |
</Text> | |
<br /> | |
<Divider /> | |
<br /> | |
<Heading size="lg">Why should we unban you?</Heading> | |
<br /> | |
<Text> | |
<i>{data.reason_for_unban}</i> | |
</Text> | |
<br /> | |
<Divider /> | |
<br /> | |
<Heading size="lg"> | |
What have you learned from your mistake? | |
</Heading> | |
<br /> | |
<Text> | |
<i>{data.learned}</i> | |
</Text> | |
</ModalBody> | |
</ModalContent>, | |
); | |
break; | |
case "inactivity": | |
setModalBody( | |
<ModalContent> | |
<ModalHeader>View Inactivity Notice</ModalHeader> | |
<ModalCloseButton /> | |
<ModalBody> | |
<Heading size="lg">Reason for Inactivity</Heading> | |
<br /> | |
<Text> | |
<i>{data.reason}</i> | |
</Text> | |
<br /> | |
<Divider /> | |
<br /> | |
<Heading size="lg">Start Date</Heading> | |
<br /> | |
<Text> | |
<i>{new Date(data.start).toLocaleDateString()}</i> | |
</Text> | |
<br /> | |
<Divider /> | |
<br /> | |
<Heading size="lg">End Date</Heading> | |
<br /> | |
<Text> | |
<i>{new Date(data.end).toLocaleDateString()}</i> | |
</Text> | |
<br /> | |
<Heading size="lg">Decisions</Heading> | |
<br /> | |
<UnorderedList> | |
{data.departments.map((d: string) => { | |
const dept = d as "DM" | "ET" | "FM" | "WM"; | |
return ( | |
<ListItem> | |
<Stack alignItems="center" direction="row"> | |
<Text>{d}: </Text> | |
{typeof data.decisions[dept] === "boolean" ? ( | |
data.decisions[dept] ? ( | |
<svg | |
fill="currentColor" | |
height="16" | |
viewBox="0 0 16 16" | |
width="16" | |
> | |
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" /> | |
</svg> | |
) : ( | |
<svg | |
fill="currentColor" | |
height="16" | |
viewBox="0 0 16 16" | |
width="16" | |
> | |
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" /> | |
</svg> | |
) | |
) : ( | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
width="16" | |
height="16" | |
fill="currentColor" | |
viewBox="0 0 16 16" | |
> | |
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z" /> | |
</svg> | |
)} | |
</Stack> | |
</ListItem> | |
); | |
})} | |
</UnorderedList> | |
</ModalBody> | |
</ModalContent>, | |
); | |
break; | |
case "report": | |
setModalBody( | |
<ModalContent> | |
<ModalHeader>View Report</ModalHeader> | |
<ModalCloseButton /> | |
<ModalBody> | |
<Heading size="lg">Username(s)</Heading> | |
<br /> | |
<Text> | |
<i>{data.target_usernames.toString()}</i> | |
</Text> | |
<br /> | |
<Divider /> | |
<br /> | |
<Heading size="lg">Description</Heading> | |
<br /> | |
<Text> | |
<i>{data.description ?? "No description"}</i> | |
</Text> | |
<br /> | |
<Divider /> | |
<br /> | |
<Heading size="lg">Media Links</Heading> | |
<br /> | |
{data.resolved_attachments.map((attachment: string) => ( | |
<Link color="#646cff" href={attachment} target="_blank"> | |
View media here | |
</Link> | |
))} | |
</ModalBody> | |
</ModalContent>, | |
); | |
break; | |
default: | |
setModalBody(<ModalContent></ModalContent>); | |
break; | |
} | |
onOpen(); | |
} | |
const { isOpen, onClose, onOpen } = useDisclosure(); | |
const [modalBody, setModalBody] = useState(<ModalContent></ModalContent>); | |
function resetModal() { | |
onClose(); | |
setModalBody(<ModalContent></ModalContent>); | |
} | |
return ( | |
<Container maxW="container.lg"> | |
<Modal isCentered isOpen={isOpen} onClose={resetModal} size="lg"> | |
<ModalOverlay /> | |
{modalBody} | |
</Modal> | |
<Heading mb={8}>My Data</Heading> | |
<br /> | |
<br /> | |
{data.permissions & (1 << 3) ? ( | |
<> | |
<Heading size="lg">Events Team Info</Heading> | |
<TableContainer mb="16px"> | |
<Table variant="simple"> | |
<TableCaption> | |
Reach out to ETM if this info is incorrect | |
</TableCaption> | |
<Thead> | |
<Tr> | |
<Th>Name</Th> | |
<Th>Points</Th> | |
<Th>Roblox ID</Th> | |
</Tr> | |
</Thead> | |
<Tbody> | |
<Tr> | |
<Td>{data.etData?.name}</Td> | |
<Td>{data.etData?.points}</Td> | |
<Td> | |
<Link | |
href={`https://www.roblox.com/users/${data.etData?.roblox_id}`} | |
> | |
{data.etData?.roblox_id} | |
</Link> | |
</Td> | |
</Tr> | |
</Tbody> | |
</Table> | |
</TableContainer> | |
</> | |
) : null} | |
<Heading size="lg">Discord Appeals</Heading> | |
<TableContainer mb="16px"> | |
<Table variant="simple"> | |
<Thead> | |
<Tr> | |
<Th>Date</Th> | |
<Th>ID</Th> | |
<Th>Status</Th> | |
<Th>View</Th> | |
</Tr> | |
</Thead> | |
<Tbody> | |
{data.items[0]?.map((result) => { | |
return ( | |
<Tr> | |
<Td>{timeStates[result.created_at].data}</Td> | |
<Td>{result.id}</Td> | |
<Td> | |
{result.open | |
? "Pending" | |
: typeof result.approved === "number" | |
? `${result.approved ? "Accepted" : "Denied"}` | |
: "Unknown"} | |
</Td> | |
<Td> | |
<Button | |
onClick={async () => await fetchItem(result.id, "appeal")} | |
> | |
View | |
</Button> | |
</Td> | |
</Tr> | |
); | |
})} | |
</Tbody> | |
</Table> | |
</TableContainer> | |
<br /> | |
{[1 << 2, 1 << 3, 1 << 9, 1 << 10].find((p) => data.permissions & p) ? ( | |
<> | |
<Heading size="lg">Inactivity Notices</Heading> | |
<TableContainer mb="16px"> | |
<Table variant="simple"> | |
<Thead> | |
<Tr> | |
<Th>Date</Th> | |
<Th>ID</Th> | |
<Th>Status</Th> | |
<Th>View</Th> | |
</Tr> | |
</Thead> | |
<Tbody> | |
{data.items[1]?.map((result) => { | |
return ( | |
<Tr> | |
<Td>{timeStates[result.created_at].data}</Td> | |
<Td>{result.id}</Td> | |
<Td>Open for details</Td> | |
<Td> | |
<Button | |
onClick={async () => | |
await fetchItem(result.id, "inactivity") | |
} | |
> | |
View | |
</Button> | |
</Td> | |
</Tr> | |
); | |
})} | |
</Tbody> | |
</Table> | |
</TableContainer> | |
<br /> | |
</> | |
) : null} | |
<Heading size="lg">Reports</Heading> | |
<TableContainer> | |
<Table variant="simple"> | |
<Thead> | |
<Tr> | |
<Th>Date</Th> | |
<Th>ID</Th> | |
<Th>Status</Th> | |
<Th>View</Th> | |
</Tr> | |
</Thead> | |
<Tbody> | |
{data.items[2]?.map((result) => { | |
return ( | |
<Tr> | |
<Td>{timeStates[result.created_at].data}</Td> | |
<Td>{result.id}</Td> | |
<Td>{result.open ? "Pending" : "Reviewed"}</Td> | |
<Td> | |
<Button | |
onClick={async () => await fetchItem(result.id, "report")} | |
> | |
View | |
</Button> | |
</Td> | |
</Tr> | |
); | |
})} | |
</Tbody> | |
</Table> | |
</TableContainer> | |
</Container> | |
); | |
} |