Remix migration
This commit is contained in:
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