Initial commit

This commit is contained in:
2023-10-19 16:49:05 -04:00
commit d731041378
63 changed files with 10037 additions and 0 deletions

44
components/Fallback.tsx Normal file
View File

@ -0,0 +1,44 @@
import { Component, type ReactNode } from "react";
import Navigation from "./Navigation";
import { Code, Container, Heading, Text } from "@chakra-ui/react";
interface ErrorState {
error: string | null;
errored: boolean;
}
type Props = {
[k: string]: any;
children: ReactNode;
};
export default class Fallback extends Component<Props, ErrorState> {
constructor(props: Props) {
super(props);
this.state = { error: null, errored: false };
}
static getDerivedStateFromError(error: Error) {
return { error, errored: true };
}
render() {
if (!this.state.errored) return this.props.children;
return (
<>
<Navigation />
<Container maxW="container.xl" pb="100px">
<Heading>Oops! Something broke.</Heading>
<Text fontSize="xl">See the details below</Text>
<br />
<Text>{this.state.error?.toString()}</Text>
</Container>
<Container maxW="container.xl">
{/* @ts-expect-error The stack property should always exist */}
<Code>{this.state.error.stack}</Code>
</Container>
</>
);
}
}

15
components/Forbidden.tsx Normal file
View File

@ -0,0 +1,15 @@
import { Container, Heading, Link, Text } from "@chakra-ui/react";
export default function () {
return (
<Container maxW="container.lg" mt="8vh" textAlign="left">
<Heading size="4xl">403</Heading>
<br />
<Text fontSize="xl">Sorry, but you aren't allowed to access that.</Text>
<br />
<Link color="#646cff" onClick={() => history.go(-1)}>
Go back
</Link>
</Container>
);
}

View File

@ -0,0 +1,195 @@
import {
Checkbox,
CheckboxGroup,
FormControl,
FormErrorMessage,
Heading,
HStack,
Input,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Radio,
RadioGroup,
Select,
Textarea,
} from "@chakra-ui/react";
import { type Dispatch, type SetStateAction, useState } from "react";
interface component {
id: string;
max_length?: number;
options?: { default?: boolean; value: string }[];
required: boolean;
title: string;
type: string;
value?: number | string | string[];
}
export default function ({
components,
read_only = true,
}: {
components: { [k: number]: component[] };
read_only: boolean;
}) {
function isNumberElemInvalid(e: HTMLInputElement): boolean {
return !(
e.value ||
e.valueAsNumber <= Number.MAX_SAFE_INTEGER ||
e.valueAsNumber >= Number.MIN_SAFE_INTEGER
);
}
function updateState(
state: { [k: string]: string | string[] },
setState: Dispatch<SetStateAction<{}>>,
id: string,
value: string
) {
const newState = { ...state };
newState[id] = value;
setState(newState);
}
function renderCheckboxOptions(
c: component,
state: { [k: string]: string | string[] },
setState: Dispatch<SetStateAction<{}>>
) {
if (!c.options) throw new Error("Options for checkbox are undefined");
const boxes = [];
const checkedBoxes = [];
for (const option of c.options) {
if (
option.default ||
(read_only && Array.isArray(c.value) && c.value.includes(option.value))
)
checkedBoxes.push(option.value);
boxes.push(
<Checkbox
isReadOnly={read_only}
onChange={(e) => {
const newState = { ...state };
const groupValues = newState[c.id] ?? [];
if (!Array.isArray(groupValues))
throw new Error("Expected CheckboxGroup values to be an array");
e.target.checked
? groupValues.push(e.target.value)
: groupValues.splice(
groupValues.findIndex((v) => v === e.target.value),
1
);
newState[c.id] = groupValues;
setState(newState);
}}
value={option.value}
>
{option.value}
</Checkbox>
);
}
return (
<CheckboxGroup defaultValue={checkedBoxes}>
<HStack spacing={5}>{boxes}</HStack>
</CheckboxGroup>
);
}
function generateReactComponents(
components: component[],
state: { [k: string]: string | string[] },
setState: Dispatch<SetStateAction<{}>>
): JSX.Element[] {
const fragmentsList = [];
for (const component of components) {
fragmentsList.push(
<Heading size="md">{component.title}</Heading>,
<br />
);
switch (component.type) {
case "checkbox":
fragmentsList.push(renderCheckboxOptions(component, state, setState));
break;
case "input":
fragmentsList.push(
<FormControl
isInvalid={
!(document.getElementById(component.id) as HTMLInputElement)
.value.length
}
isReadOnly={read_only}
>
<Input
id={component.id}
maxLength={component.max_length}
onChange={(e) =>
updateState(state, setState, component.id, e.target.value)
}
placeholder="Your response"
value={component.value}
/>
<FormErrorMessage>Field is required</FormErrorMessage>
</FormControl>
);
break;
case "number":
fragmentsList.push(
<NumberInput
isInvalid={isNumberElemInvalid(
document.getElementById(component.id) as HTMLInputElement
)}
isReadOnly={read_only}
>
<NumberInputField
id={component.id}
onChange={(e) =>
updateState(state, setState, component.id, e.target.value)
}
value={component.value}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
break;
}
fragmentsList.push(<br />, <br />, <br />);
}
return fragmentsList;
}
const pages = [];
const [responses, setResponses] = useState({});
for (const [page, componentList] of Object.entries(components)) {
pages.push(
<div
id={`form-page-${page}`}
style={{ display: page ? "none" : undefined }}
>
{generateReactComponents(componentList, responses, setResponses)}
</div>
);
}
return <></>;
}

20
components/Login.tsx Normal file
View File

@ -0,0 +1,20 @@
import { Button, Card, Container, Heading, VStack } from "@chakra-ui/react";
export default function () {
return (
<Container pt="16vh">
<Card p="4vh">
<VStack alignContent="center" gap="2vh">
<Heading>Log in to Car Crushers</Heading>
<br />
<Button
colorScheme="blue"
onClick={() => location.assign("/api/auth/oauth")}
>
Log in with Discord
</Button>
</VStack>
</Card>
</Container>
);
}

View File

@ -0,0 +1,9 @@
import { Container, Tab, TabList } from "@chakra-ui/react";
export default function () {
return (
<Container maxW="container.lg" pt="4vh">
</Container>
)
}

190
components/Navigation.tsx Normal file
View File

@ -0,0 +1,190 @@
import {
Avatar,
Box,
Button,
Center,
CloseButton,
Container,
Drawer,
DrawerContent,
DrawerOverlay,
Flex,
HStack,
Link,
Spacer,
Text,
useBreakpointValue,
useDisclosure,
} from "@chakra-ui/react";
function getAvatarUrl(userData: { [k: string]: any }): string {
const BASE = "https://cdn.discordapp.com/";
if (!userData.id) return "";
if (!userData.avatar)
return BASE + `embed/avatars/${parseInt(userData.discriminator) % 5}.png`;
return BASE + `avatars/${userData.id}/${userData.avatar}`;
}
export default function (props: {
avatar?: string;
discriminator?: string;
email?: string;
id?: string;
permissions?: number;
username?: string;
}) {
const isDesktop = useBreakpointValue({ base: false, lg: true });
const { isOpen, onClose, onOpen } = useDisclosure();
return (
<>
<Box as="section" pb={{ base: "6" }}>
<Box as="nav" boxSizing="unset">
<Container maxW="container.xl" py={{ base: "6" }}>
<Container
alignItems="center"
display={isDesktop ? "none" : "flex"}
justifyContent="space-between"
p="0"
w="calc(100vw - 6rem)"
>
<a href="/">
<img
src="/files/logo192.png"
alt="Car Crushers Logo"
style={{ width: "36px" }}
/>
</a>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
onClick={onOpen}
>
<path
fillRule="evenodd"
d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"
/>
</svg>
</Container>
<Flex
alignSelf="center"
display={isDesktop ? "flex" : "none"}
gap="0.5rem"
justifyContent="space-between"
p="0"
textAlign="center"
>
<a href="/" style={{ alignSelf: "center" }}>
<img
src="/files/logo192.png"
width="32"
alt="Car Crushers Logo"
/>
</a>
<Spacer />
<Spacer />
<Center gap="1.25rem" whiteSpace="nowrap">
<Button variant="ghost">
<Link href="/about">About Us</Link>
</Button>
<Button variant="ghost">
<Link href="/team">Our Team</Link>
</Button>
<Button variant="ghost">
<Link href="/support">Support</Link>
</Button>
<Button variant="ghost">
<Link href="/mdn">Moderation</Link>
</Button>
</Center>
<Spacer />
<Spacer />
{props.id ? (
<HStack spacing="3">
<Avatar
display={props.id ? "flex" : "none"}
src={getAvatarUrl(props)}
/>
<Text>
{props.id ? `${props.username}#${props.discriminator}` : ""}
</Text>
<Button
size="md"
style={{ display: props.id ? "block" : "none" }}
variant="ghost"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 16 16"
>
<path
fillRule="evenodd"
d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"
/>
<path
fillRule="evenodd"
d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"
/>
</svg>
</Button>
</HStack>
) : (
<Button>Log In</Button>
)}
</Flex>
</Container>
</Box>
</Box>
<Drawer isOpen={isOpen} onClose={onClose} placement="left">
<DrawerOverlay />
<DrawerContent gap="1.5vh" p="1.5vh">
<CloseButton onClick={onClose} />
<hr />
<Link href="/about">About Us</Link>
<Link href="/team">Our Team</Link>
<Link href="/support">Support</Link>
<Link href="/mdn">Moderation</Link>
<hr />
<Flex alignItems="center" gap="1rem">
<Avatar
display={props.id ? "" : "none"}
src={getAvatarUrl(props)}
/>
<Text align="center" style={{ overflowWrap: "anywhere" }}>
{props.id ? `${props.username}#${props.discriminator}` : ""}
</Text>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 16 16"
style={{
cursor: "pointer",
display: props.id ? "block" : "none",
}}
>
<path
fillRule="evenodd"
d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"
/>
<path
fillRule="evenodd"
d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"
/>
</svg>
</Flex>
</DrawerContent>
</Drawer>
</>
);
}

38
components/Success.tsx Normal file
View File

@ -0,0 +1,38 @@
import { Container, Flex, Heading, Spacer, Text } from "@chakra-ui/react";
export default function ({
heading,
message,
}: {
heading: string;
message: string;
}) {
return (
<Container
left="50%"
maxW="container.md"
pos="absolute"
top="50%"
transform="translate(-50%, -50%)"
>
<Flex>
<Spacer />
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
height="128"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M2.5 8a5.5 5.5 0 0 1 8.25-4.764.5.5 0 0 0 .5-.866A6.5 6.5 0 1 0 14.5 8a.5.5 0 0 0-1 0 5.5 5.5 0 1 1-11 0z" />
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z" />
</svg>
<Spacer />
</Flex>
<br />
<Heading>{heading}</Heading>
<br />
<Text>{message}</Text>
</Container>
);
}