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", }, ]; } 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); 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", }; useEffect(() => { 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; })(), ); }, []); 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({ bypass: false, 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++) { const reader = files[i].stream().getReader(); try { const uploadReq = await fetch(upload_urls[i], { body: supportsRequestStreams ? new ReadableStream({ async pull(controller) { const chunk = await reader.read(); if (chunk.done) { controller.close(); if (i === upload_urls.length - 1) setUploading(false); return; } controller.enqueue(chunk.value); bytesRead += chunk.value.length; setFileProgress(Math.floor((bytesRead / totalSize) * 100)); }, }) : files[i], // @ts-expect-error duplex: supportsRequestStreams ? "half" : undefined, headers: { "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], }, method: "PUT", }); if (!uploadReq.ok) { shouldRecall = true; break; } } catch (e) { console.error(e); shouldRecall = true; break; } } 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
Username(s) - To specify more than one, provide a comma-delimited list (User1, User2, User3...)
Your Evidence (Max size per file: 512MB)
Optional description