import { Button, CircularProgress, CircularProgressLabel, Container, FormControl, FormLabel, Heading, HStack, Input, Link, Text, Textarea, 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 function meta() { return [ { title: "Report an Exploiter - Car Crushers", }, { name: "description", content: "Use this page to report a cheater", }, ]; } export default function () { const [fileProgress, setFileProgress] = useState(0); const [showSuccess, setShowSuccess] = useState(false); const toast = useToast(); const [uploading, setUploading] = useState(false); const [loading, setLoading] = useState(false); const fileTypes: { [k: string]: string } = { gif: "image/gif", m4v: "video/x-m4v", mkv: "video/x-matroska", mov: "video/mp4", mp4: "video/mp4", webm: "video/webm", wmv: "video/x-ms-wmv", }; const { logged_in, site_key } = useLoaderData(); async function submit() { setLoading(true); const usernames = ( document.getElementById("usernames") as HTMLInputElement ).value .replaceAll(" ", "") .split(","); const files = (document.getElementById("evidence") as HTMLInputElement) .files; if (!usernames.length) { setLoading(false); return toast({ description: "Must provide at least one username", isClosable: true, status: "error", title: "Error", }); } if (!files?.length) { setLoading(false); return toast({ description: "Must attach at least one file", isClosable: true, status: "error", title: "Error", }); } if (usernames.length > 20) { setLoading(false); return toast({ description: "Only up to twenty users can be reported at a time", isClosable: true, status: "error", title: "Too Many Usernames", }); } let turnstileToken = ""; if (!logged_in) { const tokenElem = document .getElementsByName("cf-turnstile-response") .item(0) as HTMLInputElement; if (!tokenElem.value) { setLoading(false); return toast({ description: "Please complete the captcha and try again", isClosable: true, status: "error", title: "Captcha not completed", }); } turnstileToken = tokenElem.value; } const description = ( document.getElementById("description") as HTMLTextAreaElement ).value; const filelist = []; for (const file of files) { filelist.push({ name: file.name, size: file.size }); } const submitReq = await fetch("/api/reports/submit", { body: JSON.stringify({ description: description || undefined, files: filelist, turnstileResponse: logged_in ? undefined : turnstileToken, usernames, }), headers: { "content-type": "application/json", }, method: "POST", }); if (!submitReq.ok) { setLoading(false); if (!logged_in) { try { // @ts-expect-error turnstile.reset(); } catch {} } return toast({ description: ((await submitReq.json()) as { error: string }).error, isClosable: true, status: "error", title: "Error", }); } const { id, upload_urls }: { id: string; upload_urls: string[] } = await submitReq.json(); const totalSize = filelist.reduce((a, b) => a + b.size, 0); let bytesRead = 0; let shouldRecall = false; setUploading(true); for (let i = 0; i < upload_urls.length; i++) { await new Promise((resolve) => { const xhr = new XMLHttpRequest(); xhr.open("PUT", upload_urls[i], true); xhr.setRequestHeader( "content-type", (files[i].name.split(".").at(-1) as string).toLowerCase() === "mov" ? "video/mp4" : files[i].type || fileTypes[files[i].name.split(".").at(-1) as string], ); xhr.upload.onprogress = (e) => { if (!e.lengthComputable) return; setFileProgress( Math.floor(((bytesRead + e.loaded) / totalSize) * 100), ); }; xhr.upload.onabort = () => { shouldRecall = true; setUploading(false); setFileProgress(0); }; xhr.upload.onerror = () => { shouldRecall = true; setUploading(false); setFileProgress(0); }; xhr.upload.onloadend = (ev) => { if (i === upload_urls.length - 1) setUploading(false); bytesRead += ev.total; setFileProgress(bytesRead); resolve(null); }; xhr.send(files[i]); }); } if (shouldRecall) { setLoading(false); await fetch("/api/reports/recall", { body: JSON.stringify({ id }), headers: { "content-type": "application/json", }, method: "POST", }); // @ts-expect-error turnstile.reset(); 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", }); setShowSuccess(true); setLoading(false); } useEffect(() => { if (logged_in) return; const script = document.createElement("script"); script.async = true; script.defer = true; script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js"; document.body.appendChild(script); }, [logged_in]); return showSuccess ? ( ) : ( <> Report an Exploiter {logged_in ? null : ( Tip: Log in before submitting this report to have it appear on your data page. )}
Username(s) - To specify more than one, provide a comma-delimited list (User1, User2, User3...)
Your Evidence (Max size per file: 512MB)
Optional description