Remix migration
This commit is contained in:
19
app/context.tsx
Normal file
19
app/context.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export interface ServerStyleContextData {
|
||||
key: string;
|
||||
ids: Array<string>;
|
||||
css: string;
|
||||
}
|
||||
|
||||
export const ServerStyleContext = createContext<
|
||||
ServerStyleContextData[] | null
|
||||
>(null);
|
||||
|
||||
export interface ClientStyleContextData {
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export const ClientStyleContext = createContext<ClientStyleContextData | null>(
|
||||
null
|
||||
);
|
7
app/createEmotionCache.ts
Normal file
7
app/createEmotionCache.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import createCache from "@emotion/cache";
|
||||
|
||||
export const defaultCache = createEmotionCache();
|
||||
|
||||
export default function createEmotionCache() {
|
||||
return createCache.default({ key: "cha" });
|
||||
}
|
39
app/entry.client.tsx
Normal file
39
app/entry.client.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import { ClientStyleContext } from "./context.js";
|
||||
import createEmotionCache, { defaultCache } from "./createEmotionCache.js";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
import { RemixBrowser } from "@remix-run/react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { type ReactNode, StrictMode, useState } from "react";
|
||||
|
||||
Sentry.init({
|
||||
dsn:
|
||||
document.querySelector("meta[name='dsn']")?.getAttribute("content") ??
|
||||
undefined,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0.1,
|
||||
});
|
||||
|
||||
function ClientCacheProvider({ children }: { children: ReactNode }) {
|
||||
const [cache, setCache] = useState(defaultCache);
|
||||
|
||||
function reset() {
|
||||
setCache(createEmotionCache());
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientStyleContext.Provider value={{ reset }}>
|
||||
<CacheProvider value={cache}>{children}</CacheProvider>
|
||||
</ClientStyleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<ClientCacheProvider>
|
||||
<RemixBrowser />
|
||||
</ClientCacheProvider>
|
||||
</StrictMode>
|
||||
);
|
41
app/entry.server.tsx
Normal file
41
app/entry.server.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { CacheProvider } from "@emotion/react";
|
||||
import createEmotionCache from "./createEmotionCache.js";
|
||||
import { createEmotionServer } from "../emotion-server.js";
|
||||
import { type EntryContext } from "@remix-run/cloudflare";
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { ServerStyleContext } from "./context.js";
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
const cache = createEmotionCache();
|
||||
const { extractCriticalToChunks } = createEmotionServer(cache);
|
||||
const html = renderToString(
|
||||
<ServerStyleContext.Provider value={null}>
|
||||
<CacheProvider value={cache}>
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
</CacheProvider>
|
||||
</ServerStyleContext.Provider>
|
||||
);
|
||||
|
||||
const chunks = extractCriticalToChunks(html);
|
||||
|
||||
const markup = renderToString(
|
||||
<ServerStyleContext.Provider value={chunks.styles}>
|
||||
<CacheProvider value={cache}>
|
||||
<RemixServer context={remixContext} url={request.url} />
|
||||
</CacheProvider>
|
||||
</ServerStyleContext.Provider>
|
||||
);
|
||||
|
||||
responseHeaders.set("content-type", "text/html;charset=utf-8");
|
||||
|
||||
return new Response("<!DOCTYPE html>" + markup, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
});
|
||||
}
|
177
app/root.tsx
Normal file
177
app/root.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import {
|
||||
ChakraProvider,
|
||||
Container,
|
||||
cookieStorageManagerSSR,
|
||||
Heading,
|
||||
Link,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { ClientStyleContext, ServerStyleContext } from "./context.js";
|
||||
import fontStyle from "@fontsource/plus-jakarta-sans/index.css";
|
||||
import Forbidden from "../components/Forbidden.js";
|
||||
import globalStyles from "../index.css";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import {
|
||||
Links,
|
||||
LiveReload,
|
||||
Outlet,
|
||||
Scripts,
|
||||
useCatch,
|
||||
useLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { LinksFunction } from "@remix-run/cloudflare";
|
||||
import Login from "../components/Login.js";
|
||||
import Navigation from "../components/Navigation.js";
|
||||
|
||||
import { type ReactNode, StrictMode, useContext, useEffect } from "react";
|
||||
import theme from "../theme.js";
|
||||
import { withEmotionCache } from "@emotion/react";
|
||||
|
||||
export function CatchBoundary() {
|
||||
const { status } = useCatch();
|
||||
|
||||
switch (status) {
|
||||
case 303:
|
||||
return "";
|
||||
|
||||
case 401:
|
||||
return getMarkup({ hide: true }, <Login />);
|
||||
|
||||
case 403:
|
||||
return getMarkup({ hide: true }, <Forbidden />);
|
||||
|
||||
case 404:
|
||||
return getMarkup(
|
||||
{ hide: true },
|
||||
<Container maxW="container.lg" pt="8vh" textAlign="left">
|
||||
<Heading size="4xl">404</Heading>
|
||||
<br />
|
||||
<Text fontSize="xl">There is nothing to find here.</Text>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<Link color="#646cff" onClick={() => history.go(-1)}>
|
||||
Go back
|
||||
</Link>
|
||||
</Container>
|
||||
);
|
||||
|
||||
default:
|
||||
return getMarkup(
|
||||
{ hide: true },
|
||||
<Container maxW="container.lg" pt="8vh" textAlign="left">
|
||||
<Heading size="4xl">500</Heading>
|
||||
<br />
|
||||
<Text fontSize="xl">S̶̡͈̠̗̠͖͙̭o̶̶͕͚̥͍̪̤m̸̨͏͈͔̖͚̖̰̱͞e҉̵͖͚͇̀t̕͟͠͏͎̺̯̲̱̣̤̠̟͙̠̙̫̬ḩ̸̭͓̬͎̙̀į̞̮͉͖̰̥̹͚̫̙̪̗̜̳̕ͅn҉͔̯̪̗̝̝͖̲͇͍͎̲̲̤̖̫͈̪͡g̴̰̻̙̝͉̭͇̖̰̝̙͕̼͙͘͜ ̵̶̫̥̳̲̘̻̗͈͕̭̲͇̘̜̺̟̥̖̥b̴̙̭̹͕̞͠r̞͎̠̩͈̖̰̞̯̯͢͢͠ͅo̝̯̗̹̳͍̰͉͕̘̰̠̺̥̰͔̕ͅk̵̸̻̠͕̺̦̦͖̲̺̦̞̝̞͞͡e̶͏̤̼̼͔̘̰̰̭͈̀͞͡</Text>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<Link color="#646cff" onClick={() => location.reload()}>
|
||||
Reload
|
||||
</Link>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [
|
||||
{ href: "/favicon.ico", rel: "icon" },
|
||||
{ href: "/files/logo192.png", rel: "apple-touch-icon", type: "image/png" },
|
||||
{ href: fontStyle, rel: "stylesheet " },
|
||||
{ href: globalStyles, rel: "stylesheet" },
|
||||
];
|
||||
};
|
||||
|
||||
export async function loader({
|
||||
context,
|
||||
}: {
|
||||
context: RequestContext;
|
||||
}): Promise<{ [k: string]: any }> {
|
||||
let data: { [k: string]: string } = {};
|
||||
|
||||
if (context.data.current_user) data = { ...context.data.current_user };
|
||||
if (context.env.DSN) data.dsn = context.env.DSN;
|
||||
if (context.data.theme) data.theme = context.data.theme;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function getMarkup(
|
||||
loaderData: { [k: string]: any },
|
||||
child: ReactNode
|
||||
): JSX.Element {
|
||||
const Document = withEmotionCache(
|
||||
({ children }: { children: ReactNode }, emotionCache) => {
|
||||
const serverStyleData = useContext(ServerStyleContext);
|
||||
const clientStyleData = useContext(ClientStyleContext);
|
||||
|
||||
useEffect(() => {
|
||||
emotionCache.sheet.container = document.head;
|
||||
const tags = emotionCache.sheet.tags;
|
||||
|
||||
emotionCache.sheet.flush();
|
||||
tags.forEach((tag) => {
|
||||
(emotionCache.sheet as any)._insertTag(tag);
|
||||
});
|
||||
|
||||
clientStyleData?.reset();
|
||||
}, []);
|
||||
|
||||
const helmetContext: { [k: string]: any } = {};
|
||||
|
||||
const body = (
|
||||
<StrictMode>
|
||||
<ChakraProvider
|
||||
colorModeManager={cookieStorageManagerSSR(
|
||||
typeof document === "undefined" ? "" : document.cookie
|
||||
)}
|
||||
theme={theme}
|
||||
>
|
||||
<HelmetProvider>
|
||||
<div className="App">
|
||||
<Navigation {...loaderData} />
|
||||
{children}
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</div>
|
||||
</HelmetProvider>
|
||||
</ChakraProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
const { helmet } = helmetContext;
|
||||
|
||||
return (
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<Links />
|
||||
{serverStyleData?.map(({ key, ids, css }) => (
|
||||
<style
|
||||
key={key}
|
||||
data-emotion={`${key} ${ids.join(" ")}`}
|
||||
dangerouslySetInnerHTML={{ __html: css }}
|
||||
/>
|
||||
))}
|
||||
<meta charSet="UTF-8" />
|
||||
{loaderData.dsn ? (
|
||||
<meta name="dsn" content={loaderData.dsn} />
|
||||
) : null}
|
||||
<meta name="theme-color" content="#00a8f8" />
|
||||
{helmet.meta?.toString()}
|
||||
{helmet.title?.toString() ?? <title>Car Crushers</title>}
|
||||
</head>
|
||||
<body>{body}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return <Document>{child}</Document>;
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
|
||||
return getMarkup(loaderData, <Outlet />);
|
||||
}
|
16
app/routes/_index.tsx
Normal file
16
app/routes/_index.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { Box, Container, Text } from "@chakra-ui/react";
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<>
|
||||
<Box alignContent="left">
|
||||
<Container maxW="container.lg" paddingTop="8vh" textAlign="left">
|
||||
<Text>
|
||||
srfidukjghdiuftgrteutgrtsu,k jhsrte h hjgtsredbfdgns srthhfg h fgdyh
|
||||
y
|
||||
</Text>
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
231
app/routes/appeals.tsx
Normal file
231
app/routes/appeals.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Flex,
|
||||
Heading,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Spacer,
|
||||
Text,
|
||||
Textarea,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { useState } from "react";
|
||||
import Success from "../../components/Success.js";
|
||||
|
||||
export async function loader({ context }: { context: RequestContext }) {
|
||||
if (!context.data.current_user)
|
||||
throw new Response(null, {
|
||||
status: 401,
|
||||
});
|
||||
|
||||
const { current_user: currentUser } = context.data;
|
||||
const dataKV = context.env.DATA;
|
||||
const disabled = await dataKV.get("appeal_disabled");
|
||||
|
||||
return {
|
||||
can_appeal:
|
||||
!Boolean(disabled) &&
|
||||
!Boolean(await dataKV.get(`blockedappeal_${currentUser.id}`)) &&
|
||||
!Boolean(
|
||||
(
|
||||
await dataKV.list({
|
||||
prefix: `appeal_${currentUser.id}`,
|
||||
})
|
||||
).keys.find((appeal) => (appeal.metadata as { [k: string]: any }).open)
|
||||
),
|
||||
can_toggle:
|
||||
currentUser.permissions & (1 << 0) || currentUser.permissions & (1 << 11),
|
||||
disabled: Boolean(disabled),
|
||||
};
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const pageProps = useLoaderData<typeof loader>();
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
async function submit() {
|
||||
const learned = (document.getElementById("learned") as HTMLInputElement)
|
||||
.value;
|
||||
const whyBanned = (document.getElementById("whyBanned") as HTMLInputElement)
|
||||
.value;
|
||||
const whyUnban = (document.getElementById("whyUnban") as HTMLInputElement)
|
||||
.value;
|
||||
|
||||
const submitReq = await fetch("/api/appeals/submit", {
|
||||
body: JSON.stringify({
|
||||
learned,
|
||||
whyBanned,
|
||||
whyUnban,
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
}).catch(() => {});
|
||||
|
||||
if (!submitReq)
|
||||
return toast({
|
||||
description: "Please check your internet and try again",
|
||||
duration: 10000,
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Request Failed",
|
||||
});
|
||||
|
||||
if (!submitReq.ok)
|
||||
return toast({
|
||||
description: ((await submitReq.json()) as { error: string }).error,
|
||||
duration: 10000,
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Error",
|
||||
});
|
||||
|
||||
setShowSuccess(true);
|
||||
}
|
||||
|
||||
async function toggle(active: boolean) {
|
||||
const toggleReq = await fetch("/api/appeals/toggle", {
|
||||
body: JSON.stringify({ active }),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!toggleReq.ok)
|
||||
return toast({
|
||||
description: ((await toggleReq.json()) as { error: string }).error,
|
||||
duration: 10000,
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Error",
|
||||
});
|
||||
|
||||
toast({
|
||||
description: `The appeals form is now ${active ? "opened" : "closed"}.`,
|
||||
isClosable: true,
|
||||
status: "success",
|
||||
title: `Appeals ${active ? "enabled" : "disabled"}`,
|
||||
});
|
||||
|
||||
onClose();
|
||||
await new Promise((p) => setTimeout(p, 5000));
|
||||
}
|
||||
|
||||
return showSuccess ? (
|
||||
<Success
|
||||
heading="Appeal Submitted"
|
||||
message="You will receive an email when we reach a decision."
|
||||
/>
|
||||
) : (
|
||||
<Container maxW="container.md" pt="4vh" textAlign="start">
|
||||
<Alert
|
||||
borderRadius="8px"
|
||||
display={pageProps.disabled ? "flex" : "none"}
|
||||
mb="16px"
|
||||
status="error"
|
||||
>
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>Appeals Closed</AlertTitle>
|
||||
<AlertDescription>
|
||||
We are currently not accepting appeals.
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
<Flex>
|
||||
<Spacer />
|
||||
<Button display={pageProps.can_toggle ? "" : "none"} onClick={onOpen}>
|
||||
{pageProps.disabled ? "Enable" : "Disable"} Appeals
|
||||
</Button>
|
||||
</Flex>
|
||||
<br />
|
||||
<Modal isCentered isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Toggle appeals?</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text>
|
||||
Are you sure you want to{" "}
|
||||
{pageProps.disabled ? "enable" : "disable"} appeals?
|
||||
</Text>
|
||||
</ModalBody>
|
||||
<ModalFooter style={{ gap: "8px" }}>
|
||||
<Button onClick={onClose} variant="ghost">
|
||||
No
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => await toggle(pageProps.disabled)}
|
||||
variant="danger"
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Heading size="xl">Discord Appeals</Heading>
|
||||
<br />
|
||||
<Text fontSize="md">
|
||||
This is for Discord bans only! See the support page if you were banned
|
||||
from the game.
|
||||
</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="md">Why were you banned?</Heading>
|
||||
<br />
|
||||
<Textarea
|
||||
disabled={!pageProps.can_appeal}
|
||||
id="whyBanned"
|
||||
maxLength={500}
|
||||
placeholder="Your response"
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="md">Why should we unban you?</Heading>
|
||||
<br />
|
||||
<Textarea
|
||||
disabled={!pageProps.can_appeal}
|
||||
id="whyUnban"
|
||||
maxLength={2000}
|
||||
placeholder="Your response"
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="md">What have you learned from your mistake?</Heading>
|
||||
<br />
|
||||
<Textarea
|
||||
disabled={!pageProps.can_appeal}
|
||||
id="learned"
|
||||
maxLength={2000}
|
||||
placeholder="Your response"
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
<Button
|
||||
disabled={pageProps.can_appeal}
|
||||
onClick={async () => await submit()}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
140
app/routes/mod-queue.tsx
Normal file
140
app/routes/mod-queue.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Flex,
|
||||
Select,
|
||||
useBreakpointValue,
|
||||
useToast,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { lazy, useState } from "react";
|
||||
import AppealCard from "../../components/AppealCard.js";
|
||||
import Login from "../../components/Login.js";
|
||||
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 newItemPermissions = {
|
||||
game_ban: [1 << 5],
|
||||
inactivity: [1 << 2, 1 << 9, 1 << 10],
|
||||
infraction: [1 << 0, 1 << 2, 1 << 6, 1 << 7],
|
||||
};
|
||||
|
||||
const newItemNames: { [k: string]: string } = {
|
||||
game_ban: "Game Ban",
|
||||
inactivity: "Inactivity Notice",
|
||||
infraction: "Infraction",
|
||||
};
|
||||
|
||||
const typePermissions = {
|
||||
appeal: [1 << 0, 1 << 1],
|
||||
gma: [1 << 5],
|
||||
report: [1 << 5],
|
||||
};
|
||||
|
||||
const typeNames: { [k: string]: string } = {
|
||||
appeal: "Discord Appeals",
|
||||
gma: "Game Appeals",
|
||||
report: "Game Reports",
|
||||
};
|
||||
|
||||
const allowedNewItems = [];
|
||||
const allowedTypes = [];
|
||||
|
||||
for (const [item, ints] of Object.entries(newItemPermissions)) {
|
||||
if (ints.find((i) => currentUser.permissions & i))
|
||||
allowedNewItems.push({ name: newItemNames[item], value: item });
|
||||
}
|
||||
|
||||
for (const [type, ints] of Object.entries(typePermissions)) {
|
||||
if (ints.find((i) => currentUser.permissions & i))
|
||||
allowedTypes.push({ name: typeNames[type], value: type });
|
||||
}
|
||||
|
||||
if (!allowedTypes.length)
|
||||
throw new Response(null, {
|
||||
status: 403,
|
||||
});
|
||||
|
||||
return {
|
||||
entry_types: allowedTypes,
|
||||
item_types: allowedNewItems,
|
||||
};
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const pageProps = useLoaderData<typeof loader>();
|
||||
const isDesktop = useBreakpointValue({ base: false, lg: true });
|
||||
const entryTypes = [];
|
||||
const [entries, setEntries] = useState([] as JSX.Element[]);
|
||||
|
||||
for (const type of pageProps.entry_types)
|
||||
entryTypes.push(<option value={type.value}>{type.name}</option>);
|
||||
|
||||
async function updateQueue(
|
||||
queue_type: string,
|
||||
show_closed: boolean = false
|
||||
): Promise<void> {
|
||||
const queueReq = await fetch(
|
||||
`/api/mod-queue/list?type=${queue_type}&showClosed=${show_closed}`
|
||||
);
|
||||
|
||||
if (!queueReq.ok) {
|
||||
const errorData: { error: string } = await queueReq.json();
|
||||
|
||||
useToast()({
|
||||
description: errorData.error,
|
||||
duration: 10000,
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Failed to load queue",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const entryData: { [k: string]: any }[] = await queueReq.json();
|
||||
const newEntries = [];
|
||||
|
||||
for (const entry of entryData) {
|
||||
switch (queue_type) {
|
||||
case "appeal":
|
||||
newEntries.push(
|
||||
<AppealCard
|
||||
{...(entry as {
|
||||
ban_reason: string;
|
||||
createdAt: number;
|
||||
discriminator: string;
|
||||
id: string;
|
||||
learned: string;
|
||||
reason_for_unban: string;
|
||||
username: string;
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setEntries(newEntries);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxW="container.lg">
|
||||
<Flex>
|
||||
<VStack w="container.md">{entries}</VStack>
|
||||
<Box display={isDesktop ? undefined : "none"} w="256px">
|
||||
<Select placeholder="Entry Type">
|
||||
<option value="">All</option>
|
||||
{entryTypes}
|
||||
</Select>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
}
|
238
app/routes/privacy.tsx
Normal file
238
app/routes/privacy.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
import { Container, Heading, Link, Text } from "@chakra-ui/react";
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<Container maxW="container.lg" pb="8vh" pt="4vh" textAlign="start">
|
||||
<Heading>Privacy Policy</Heading>
|
||||
<br />
|
||||
<Text>Last Updated: 2023-01-07</Text>
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
<Heading size="lg">Information We Collect</Heading>
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Discord Profile Information</strong>: We receive account
|
||||
information from Discord, Inc. when you sign in such as your username,
|
||||
discriminator, avatar, and banner in order to authenticate you and
|
||||
authorize requests. A list of information available can be found at{" "}
|
||||
<Link
|
||||
color="#646cff"
|
||||
href="https://discord.com/developers/docs/resources/user"
|
||||
target="_blank"
|
||||
>
|
||||
Discord's Developer Portal
|
||||
</Link>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Email Addresses</strong>: We receive your email address from
|
||||
your Discord account in order to allow us to contact you as necessary.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Member Information from Our Discord Server</strong>: We
|
||||
receive a list of your roles within our Discord Server in order to
|
||||
determine your ability to access certain parts of this site.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Error Reports</strong>: We collect error reports to aid in
|
||||
fixing bugs and ensuring site stability.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Uploaded Files and Reports</strong>: We store uploaded files
|
||||
and user reports to aid in moderating exploiters from Car Crushers 2.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Appeals</strong>: We store appeal requests to review them.
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<Heading size="lg">
|
||||
Legal Basis for Processing of Your Personal Data
|
||||
</Heading>
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<strong>To fulfill contractual commitments</strong>: E.g allow you to
|
||||
use the site as intended.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Legitimate interests</strong>: In some cases, we continue to
|
||||
process data on the grounds that our legitimate interests override the
|
||||
interests or rights and freedoms of affected individuals. These
|
||||
interests may include but are not limited to: Protecting ourselves and
|
||||
our users, and preventing spam.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Consent</strong>: Where required by law, and in some other
|
||||
cases, we handle personal data on the basis of your implied or express
|
||||
consent.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Legal Requirements</strong>: We need to use and disclose
|
||||
personal data in certain ways to comply with our legal obligations.
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<Heading size="lg">Disclosure of Your Information</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
While we have no intention of giving your personal data to Mark
|
||||
Zuckerberg, in certain circumstances we may share your information with
|
||||
third parties, as set forth below.
|
||||
</Text>
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Business Transfers</strong>: In the event of a corporate sale,
|
||||
merger, reorganization, bankruptcy, dissolution or etc., your
|
||||
information may be part of the transferred assets.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Consent</strong>: We may transfer your information with your
|
||||
consent.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Agents and Related Third Parties</strong>: We enlist the help
|
||||
of other entities to perform certain tasks related to this site, such
|
||||
as: Data Storage, Error Tracking and Reporting, and Hosting.
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<Heading size="lg">Our Partners</Heading>
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Cloudflare, Inc. (San Francisco, CA)</strong>: Hosting and
|
||||
data storage provider.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Google, LLC. (Mountain View, CA)</strong>: File storage.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Mailgun Technologies, Inc. (San Antonio, TX)</strong>: Email
|
||||
solutions.
|
||||
</li>
|
||||
<li>
|
||||
<strong>
|
||||
Functional Software, Inc. d/b/a Sentry (San Francisco, CA)
|
||||
</strong>
|
||||
: Error reporting.
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<Heading size="lg">Security</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
We take reasonable steps to protect the information provided via the
|
||||
Services from loss, misuse, and unauthorized access, disclosure,
|
||||
alteration, or destruction. However, no Internet or email transmission
|
||||
is ever fully secure or error free. In particular, email sent to or from
|
||||
the Services may not be secure. Therefore, you should take special care
|
||||
in deciding what information you send to us via email or forms. Please
|
||||
keep this in mind when disclosing any information via the Internet.
|
||||
</Text>
|
||||
<br />
|
||||
<Heading size="lg">Cookies</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
All cookies by this site are necessary for the site to function. We do
|
||||
not set any marketing or tracking cookies.
|
||||
</Text>
|
||||
<br />
|
||||
<Heading size="lg">Children</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
Our services are meant for users who meet or exceed the age of digital
|
||||
consent under relevant laws (such as COPPA and GDPR).
|
||||
</Text>
|
||||
<br />
|
||||
<Heading size="lg">Links to Other Websites</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
This privacy policy only applies to this site and not other sites.
|
||||
</Text>
|
||||
<br />
|
||||
<Heading size="lg">Data Retention</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
All data is retained as stated in the section below and is necessary to
|
||||
conduct operations.
|
||||
</Text>
|
||||
<br />
|
||||
<Heading size="lg">Data Retention Periods</Heading>
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Session Data</strong>: Maximum of one week (or on logout).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Email Addresses</strong>: From the time that you submit a form
|
||||
to the time that it is marked as closed (i.e when your appeal is
|
||||
reviewed).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Profile, Server, and Permission Data</strong>: See the Session
|
||||
Data entry.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Error Reports</strong>: Error reports collected by Sentry are
|
||||
deleted after 90 days. Sensitive information such as IP addresses are
|
||||
not retained with logs.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Uploaded Files and Report Submissions</strong>: Report
|
||||
submissions and uploaded media as part of a report submission may be
|
||||
retained indefinitely. Email addresses are removed after report
|
||||
review.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Appeals</strong>: Appeal submissions are retained for 3 years
|
||||
after closure date.
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<Heading size="lg">Data Rights and Choices</Heading>
|
||||
<br />
|
||||
<Text>You have the right to:</Text>
|
||||
<br />
|
||||
<ul>
|
||||
<li>
|
||||
Access your personal information (or export it in a portable format)
|
||||
</li>
|
||||
<li>Delete your personal information</li>
|
||||
<li>
|
||||
Request restrictions on the processing of your personal information
|
||||
</li>
|
||||
<li>Lodge a complaint with a supervisory authority</li>
|
||||
</ul>
|
||||
<br />
|
||||
<Text>
|
||||
Some information as specified in the Data Retention section may be
|
||||
retained even after deleting your data. The rights and options listed
|
||||
above are subject to limitations based on applicable law.
|
||||
</Text>
|
||||
<br />
|
||||
<Heading size="lg">Contact Us</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
By Email:{" "}
|
||||
<Link color="#646cff" href="mailto:privacy@ccdiscussion.com">
|
||||
privacy@ccdiscussion.com
|
||||
</Link>
|
||||
</Text>
|
||||
<br />
|
||||
<Text>
|
||||
Discord:{" "}
|
||||
<Link
|
||||
color="#646cff"
|
||||
href="https://discord.com/invite/carcrushers"
|
||||
target="_blank"
|
||||
>
|
||||
https://discord.com/invite/carcrushers
|
||||
</Link>
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
254
app/routes/report.tsx
Normal file
254
app/routes/report.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
import {
|
||||
Button,
|
||||
CircularProgress,
|
||||
CircularProgressLabel,
|
||||
Container,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Heading,
|
||||
HStack,
|
||||
Input,
|
||||
Link,
|
||||
Text,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import Success from "../../components/Success.js";
|
||||
|
||||
export async function loader({
|
||||
context,
|
||||
}: {
|
||||
context: RequestContext;
|
||||
}): Promise<{ logged_in: boolean; site_key: string }> {
|
||||
return {
|
||||
logged_in: Boolean(context.data.current_user),
|
||||
site_key: context.env.TURNSTILE_SITEKEY,
|
||||
};
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const [fileProgress, setFileProgress] = useState(0);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [supportsRequestStreams, setSupportsRequestStreams] = useState(false);
|
||||
const toast = useToast();
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem("REPORT_SUCCESS")) {
|
||||
sessionStorage.removeItem("REPORT_SUCCESS");
|
||||
return setShowSuccess(true);
|
||||
}
|
||||
|
||||
setSupportsRequestStreams(
|
||||
(() => {
|
||||
let duplexAccessed = false;
|
||||
|
||||
const hasContentType = new Request("", {
|
||||
body: new ReadableStream(),
|
||||
method: "POST",
|
||||
// @ts-ignore
|
||||
get duplex() {
|
||||
duplexAccessed = true;
|
||||
return "half";
|
||||
},
|
||||
}).headers.has("Content-Type");
|
||||
|
||||
return duplexAccessed && !hasContentType;
|
||||
})()
|
||||
);
|
||||
}, []);
|
||||
|
||||
async function submit() {
|
||||
const usernames = (
|
||||
document.getElementById("usernames") as HTMLInputElement
|
||||
).value
|
||||
.replaceAll(" ", "")
|
||||
.split(",");
|
||||
const file = (
|
||||
document.getElementById("evidence") as HTMLInputElement
|
||||
).files?.item(0);
|
||||
|
||||
if (!usernames.length)
|
||||
return toast({
|
||||
description: "Must provide at least one username",
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Error",
|
||||
});
|
||||
|
||||
if (!file)
|
||||
return toast({
|
||||
description: "Must attach a file",
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Error",
|
||||
});
|
||||
|
||||
if (usernames.length > 20)
|
||||
return toast({
|
||||
description: "Only up to twenty users can be reported at a time",
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Too Many Usernames",
|
||||
});
|
||||
|
||||
const submitReq = await fetch("/api/reports/submit", {
|
||||
body: JSON.stringify({
|
||||
filename: file.name,
|
||||
filesize: file.size,
|
||||
usernames,
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!submitReq.ok)
|
||||
return toast({
|
||||
description: ((await submitReq.json()) as { error: string }).error,
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Error",
|
||||
});
|
||||
|
||||
const { id, upload_url }: { id: string; upload_url: string } =
|
||||
await submitReq.json();
|
||||
|
||||
setUploading(true);
|
||||
const reader = file.stream().getReader();
|
||||
let bytesRead = 0;
|
||||
|
||||
const uploadReq = await fetch(upload_url, {
|
||||
body: supportsRequestStreams
|
||||
? new ReadableStream({
|
||||
async pull(controller) {
|
||||
const chunk = await reader.read();
|
||||
|
||||
if (chunk.done) {
|
||||
controller.close();
|
||||
setUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.enqueue(chunk.value);
|
||||
bytesRead += chunk.value.length;
|
||||
setFileProgress(Math.floor((bytesRead / file.size) * 100));
|
||||
},
|
||||
})
|
||||
: file,
|
||||
// @ts-expect-error
|
||||
duplex: supportsRequestStreams ? "half" : undefined,
|
||||
headers: {
|
||||
"content-type": file.type,
|
||||
},
|
||||
method: "PUT",
|
||||
}).catch(console.error);
|
||||
|
||||
if (!uploadReq?.ok) {
|
||||
await fetch("/api/reports/recall", {
|
||||
body: JSON.stringify({ id }),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
return toast({
|
||||
description: "Failed to upload file",
|
||||
isClosable: true,
|
||||
status: "error",
|
||||
title: "Error",
|
||||
});
|
||||
}
|
||||
|
||||
await fetch("/api/reports/complete", {
|
||||
body: JSON.stringify({ id }),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
sessionStorage.setItem("REPORT_SUCCESS", "1");
|
||||
}
|
||||
|
||||
const { logged_in, site_key } = useLoaderData<typeof loader>();
|
||||
|
||||
return showSuccess ? (
|
||||
<Success
|
||||
heading="Report Submitted"
|
||||
message="We will review it as soon as possible."
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Container maxW="container.md" pt="4vh" textAlign="start">
|
||||
<Heading mb="4vh">Report an Exploiter</Heading>
|
||||
<br />
|
||||
<FormControl isRequired>
|
||||
<FormLabel>
|
||||
Username(s) - To specify more than one, provide a comma-delimited
|
||||
list (User1, User2, User3...)
|
||||
</FormLabel>
|
||||
<Input id="usernames" placeholder="builderman" />
|
||||
</FormControl>
|
||||
<br />
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Your Evidence (Max Size: 512MB)</FormLabel>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
mr="8px"
|
||||
onClick={() => document.getElementById("evidence")?.click()}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
<input id="evidence" type="file" />
|
||||
</FormControl>
|
||||
<br />
|
||||
<br />
|
||||
<Text>
|
||||
By submitting this form, you agree to the{" "}
|
||||
<Link color="#646cff" href="/terms">
|
||||
Terms of Service
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link color="#646cff" href="/privacy">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
<br />
|
||||
<HStack>
|
||||
<Button
|
||||
disabled={uploading}
|
||||
mr="8px"
|
||||
onClick={async () => await submit()}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<CircularProgress
|
||||
display={uploading ? "" : "none"}
|
||||
isIndeterminate={!supportsRequestStreams}
|
||||
value={supportsRequestStreams ? fileProgress : undefined}
|
||||
>
|
||||
{supportsRequestStreams ? (
|
||||
<CircularProgressLabel>{fileProgress}%</CircularProgressLabel>
|
||||
) : null}
|
||||
</CircularProgress>
|
||||
</HStack>
|
||||
<div
|
||||
className="cf-turnstile"
|
||||
data-sitekey={useLoaderData<typeof loader>()}
|
||||
></div>
|
||||
</Container>
|
||||
{logged_in ? null : (
|
||||
<script
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
136
app/routes/support.tsx
Normal file
136
app/routes/support.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Container,
|
||||
Heading,
|
||||
Link,
|
||||
Spacer,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<Container
|
||||
borderRadius="12px"
|
||||
borderWidth="1px"
|
||||
maxW="container.md"
|
||||
mt="8vh"
|
||||
>
|
||||
<VStack w="100%" spacing={3}>
|
||||
<Spacer />
|
||||
<Heading alignSelf="start" pl="2.5%" size="md">
|
||||
What do you need help with?
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Accordion textAlign="left" w="100%">
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
I want to report someone exploiting
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
To report a player,{" "}
|
||||
<Link color="#646cff" href="/report">
|
||||
head to our report page.
|
||||
</Link>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
I want a data rollback or transfer
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
Please join our{" "}
|
||||
<Link
|
||||
color="#646cff"
|
||||
href="https://discord.com/invite/carcrushers"
|
||||
>
|
||||
Discord server
|
||||
</Link>{" "}
|
||||
and contact ModMail.
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
I want to appeal my ban
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
If you were banned from our Discord server,{" "}
|
||||
<Link color="#646cff" href="/appeals">
|
||||
use this form
|
||||
</Link>
|
||||
. If you were banned from the game,{" "}
|
||||
<Link
|
||||
color="#646cff"
|
||||
href="https://www.roblox.com/games/527921900/Car-Crushers-2-Appeals"
|
||||
>
|
||||
fill out the form here
|
||||
</Link>
|
||||
.
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
I want to apply for a staff position
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
Most staff position openings will be announced in our{" "}
|
||||
<Link
|
||||
color="#646cff"
|
||||
href="https://discord.com/invite/carcrushers"
|
||||
>
|
||||
Discord server
|
||||
</Link>
|
||||
. Forum mod openings are generally announced on the forum.
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
My problem is not listed
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
Join our{" "}
|
||||
<Link
|
||||
color="#646cff"
|
||||
href="https://discord.com/invite/carcrushers"
|
||||
>
|
||||
Discord server
|
||||
</Link>{" "}
|
||||
and open a ticket with ModMail.
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<Spacer />
|
||||
<Spacer />
|
||||
</VStack>
|
||||
</Container>
|
||||
);
|
||||
}
|
54
app/routes/team.tsx
Normal file
54
app/routes/team.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import {
|
||||
Card,
|
||||
CardFooter,
|
||||
Code,
|
||||
Container,
|
||||
Divider,
|
||||
Heading,
|
||||
Image,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import team from "../../data/team.json";
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<Container maxW="container.xl" pt="4vh">
|
||||
<Heading textAlign="start">Our Team</Heading>
|
||||
<br />
|
||||
<Text textAlign="start">
|
||||
Please respect our staff, and <u>do not send direct messages</u> or
|
||||
friend requests in place of official channels.
|
||||
</Text>
|
||||
<br />
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: "1rem",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(16rem, 1fr))",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{team.map((member) => (
|
||||
<Card key={member.id} maxW="xs" p="12px">
|
||||
<Image
|
||||
alt={member.tag + "'s avatar"}
|
||||
borderRadius="50%"
|
||||
src={`/files/avatars/${member.id}.webp`}
|
||||
/>
|
||||
<Stack mb="8" mt="6" spacing="3">
|
||||
<b>
|
||||
<Heading size="md">{member.tag}</Heading>
|
||||
</b>
|
||||
<Text>{member.position}</Text>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<CardFooter justifyContent="center">
|
||||
<Code>ID: {member.id}</Code>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
120
app/routes/terms.tsx
Normal file
120
app/routes/terms.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { Container, Heading, Link, Text } from "@chakra-ui/react";
|
||||
|
||||
export default function () {
|
||||
return (
|
||||
<Container maxW="container.lg" pb="8vh" pt="4vh" textAlign="start">
|
||||
<Heading>Terms and Conditions</Heading>
|
||||
<br />
|
||||
<Text>Last Updated: 2023-01-07</Text>
|
||||
<br />
|
||||
<Text>Yes, we know this shit is boring to read, but it's important.</Text>
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
<Text>These terms govern your use of the Car Crushers website.</Text>
|
||||
<br />
|
||||
<Text>
|
||||
You would think people have common sense but sadly many don't.
|
||||
</Text>
|
||||
<br />
|
||||
<Text>For this reason, we have to create this document.</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="lg">Definitions</Heading>
|
||||
<br />
|
||||
<ul>
|
||||
<li>We, Us: Car Crushers (the operator of this website)</li>
|
||||
<li>You: The person currently reading this document</li>
|
||||
</ul>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="lg">General Rules</Heading>
|
||||
<br />
|
||||
<ul>
|
||||
<li>Do not upload malicious files to this site</li>
|
||||
<li>Do not submit spam using forms on this site</li>
|
||||
<li>Do not upload any content illegal under the laws of Sweden</li>
|
||||
<li>You must be at least 13 years old to use this site</li>
|
||||
<li>
|
||||
You may not automate access to this site by any means (except a public
|
||||
search crawler if you operate one)
|
||||
</li>
|
||||
<li>
|
||||
You may not falsely imply that you are affiliated with or endorsed by
|
||||
Car Crushers
|
||||
</li>
|
||||
<li>
|
||||
<Link color="#646cff" href="/files/why.jpg" target="_blank">
|
||||
All visitors from New Jersey must explain why
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="lg">Enforcement</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
We may investigate and prosecute violations of these terms to the
|
||||
fullest legal extent. We may notify and cooperate with law enforcement
|
||||
authorities in prosecuting violations of the law and these terms.
|
||||
</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="lg">Your Content</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
Nothing in these terms grant us ownership rights to any content that you
|
||||
submit to this site. Nothing in these terms grants you ownership rights
|
||||
to our intellectual property either. Any content you submit to this site
|
||||
is your responsibility. Content you submit to us belongs to you. But at
|
||||
a minimum, you license us to store the content and display it to
|
||||
authorized users.
|
||||
</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="lg">Warranty and Disclaimer</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
WE DO NOT GUARANTEE A BUG-FREE SITE, THAT IS IMPOSSIBLE. THERE IS
|
||||
ABSOLUTELY NO WARRANTY WHATSOEVER, EXPRESS OR IMPLIED. YOUR USE OF THIS
|
||||
SITE IS AT YOUR OWN RISK. THERE ARE NO GUARANTEES ON ANYTHING, NOT EVEN
|
||||
THAT THIS SITE WILL EXIST TOMORROW.
|
||||
</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="lg">Termination</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
We may terminate or suspend your access to the Service immediately,
|
||||
without prior notice or liability, under our sole discretion, for any
|
||||
reason whatsoever and without limitation, including but not limited to a
|
||||
breach of the Terms.
|
||||
</Text>
|
||||
<br />
|
||||
<Text>
|
||||
The contract is fully terminated when all user data (including every
|
||||
copy of every file uploaded) is fully deleted from our service.
|
||||
</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="lg">Indemnification</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
You agree to defend, indemnify and hold harmless Car Crushers and its
|
||||
licensee and licensors, and their employees, contractors, agents,
|
||||
officers and directors, from and against any and all claims, damages,
|
||||
obligations, losses, liabilities, costs or debt, and expenses (including
|
||||
but not limited to attorney's fees), resulting from or arising out of a)
|
||||
your use and access of the Service, or b) a breach of these Terms.
|
||||
</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Heading size="lg">Governing Law</Heading>
|
||||
<br />
|
||||
<Text>
|
||||
These terms shall be governed and construed in accordance with the laws
|
||||
of Västmanland, Sweden.
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user