Initial commit
This commit is contained in:
44
components/Fallback.tsx
Normal file
44
components/Fallback.tsx
Normal 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
15
components/Forbidden.tsx
Normal 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>
|
||||
);
|
||||
}
|
195
components/FormGenerator.tsx
Normal file
195
components/FormGenerator.tsx
Normal 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
20
components/Login.tsx
Normal 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>
|
||||
);
|
||||
}
|
9
components/ModWrapper.tsx
Normal file
9
components/ModWrapper.tsx
Normal 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
190
components/Navigation.tsx
Normal 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
38
components/Success.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user