From 93dfe2be64e8658839bcfe5356adf35f8cde7075 Mon Sep 17 00:00:00 2001
From: Nicolas James <Eele1Ephe7uZahRie@tutanota.com>
Date: Thu, 13 Feb 2025 18:04:18 +1100
Subject: initial commit

---
 src/web/components/submission/Submission.jsx | 221 +++++++++++++++++++++++++++
 1 file changed, 221 insertions(+)
 create mode 100644 src/web/components/submission/Submission.jsx

(limited to 'src/web/components/submission/Submission.jsx')

diff --git a/src/web/components/submission/Submission.jsx b/src/web/components/submission/Submission.jsx
new file mode 100644
index 0000000..9fc080e
--- /dev/null
+++ b/src/web/components/submission/Submission.jsx
@@ -0,0 +1,221 @@
+import React, {useEffect, useState, useContext} from "react";
+import {useLocation} from "react-router-dom";
+import {encode} from "base64-arraybuffer";
+
+import {getSubreactFromLocation} from "helpers/Location.jsx";
+import {UserContext} from "contexts/UserContext.jsx";
+import {StateContext} from "contexts/StateContext.jsx";
+
+import Card from "components/card/Card.jsx";
+import Button from "components/button/Button.jsx";
+import Input from "components/input/Input.jsx";
+import Loading from "components/loading/Loading.jsx";
+
+import styles from "./Submission.css";
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
+import {faMinus, faPlus} from "@fortawesome/free-solid-svg-icons";
+import {getFieldError, correctFieldAnimate, isSupportedExtension} from "helpers/Input.jsx";
+
+const maxFileSize = 4 * 1000 * 1000; // 4MB max size
+
+const validateTitle = (title) => getFieldError(title?.target?.value, [3, 128]);
+const validateContents = (contents) => getFieldError(contents?.target?.value, [5, 3000]);
+function validateFile(file) {
+    const files = file?.target?.files;
+    if (files == null || files.length == 0) {
+        return "Required";
+    }
+    const f = files.item(0);
+    if (f.size > maxFileSize) { // 4MB
+        return "File too large, exceeded 4MB";
+    }
+    const ext = f.name.split(".")?.pop()?.toLowerCase();
+    if (!isSupportedExtension(ext)) {
+        return "Unsupported image extension";
+    }
+    return null;
+}
+
+function ErrorMessage({message}) {
+    return (
+        <div className={styles.error + (message != null ? " " + styles.errorActive : "")}>
+            <p>
+                {message}
+            </p>
+        </div>
+    );
+}
+
+function RequestButton({promise, setPromise, title, setTitle, contents, setContents,
+                        file, setFile, setError, className, children, setSubmissionActive, subreact}) {
+    const onClick = (event) => {
+        event.preventDefault();
+        if (promise != null) {
+            return;
+        }
+        setError(null);
+
+        const titleErrors = validateTitle(title);
+        const contentsErrors = validateContents(contents);
+        const fileErrors = validateFile(file);
+        if (titleErrors != null || contentsErrors != null || fileErrors != null) {
+            correctFieldAnimate(title, setTitle, titleErrors);
+            correctFieldAnimate(contents, setContents, contentsErrors);
+            correctFieldAnimate(file, setFile, fileErrors);
+            return;
+        }
+        
+        setPromise(file.target.files.item(0).arrayBuffer()
+            .then((buffer) => {
+                const init = {
+                    method: "POST",
+                    headers: {
+                        'Content-Type': "application/json"
+                    },
+                    body: JSON.stringify({
+                        "title": title.target.value,
+                        "contents": contents.target.value,
+                        "subreact": subreact,
+                        "file": encode(buffer),
+                    }),
+                    referrer: "same-origin",
+                };
+                return fetch("/api/post", init);
+            })
+            .then((response) => {
+                if (response.ok) {
+                    setSubmissionActive(false);
+                    setContents(null);
+                    setTitle(null);
+                    setFile(null);
+                    return;
+                }
+
+                return response.json()
+                    .then((json) => {
+                        if ("error" in json) {
+                            throw new Error(json.error);
+                        }
+                        throw new Error("unexpected data received");
+                    });
+            })
+            .catch((error) => {
+                console.log(error);
+                setError(error.message);
+            })
+            .finally(() => {
+                setPromise(null);
+            }));
+    }
+
+    return (
+        <Button className={className} onClick={onClick}>
+            {promise == null ? children : <Loading className={styles.loading} />}
+        </Button>
+    );
+}
+
+function TabIcon({icon, submissionActive, isDefault}) {
+    return (
+        <FontAwesomeIcon icon={icon} 
+            className={styles.tabIcon + 
+                (!isDefault == submissionActive ? " " + styles.tabIconHidden : "")} />
+    );
+}
+
+function Tab({submissionActive, setSubmissionActive, user, setLoginActive}) {
+    const [animate, setAnimate] = useState(false);
+
+    const onClick = () => {
+        if (user === null) {
+            setLoginActive(true);
+            return;
+        }
+        setAnimate(true);
+        setSubmissionActive(!submissionActive);
+    };
+
+    return (
+        <div className={styles.tabContainer}>
+            <div className={styles.tab + (animate ? " " + styles.fade : "")} onClick={onClick}>
+                <TabIcon icon={faMinus} submissionActive={submissionActive} isDefault />
+                <TabIcon icon={faPlus} submissionActive={submissionActive} />
+            </div>
+        </div>
+    );
+}
+
+function SubmissionForm({setSubmissionActive, setError, subreact}) {
+    const [promise, setPromise] = useState(null);
+    const [title, setTitle] = useState(null);
+    const [contents, setContents] = useState(null);
+    const [file, setFile] = useState(null);
+
+    return (
+        <form>
+            <Input type="text" 
+                id="title"
+                title="Title"
+                field={title}
+                setField={setTitle}
+                validateField={validateTitle} />
+
+            <Input className={styles.contentsInput}
+                inputType="textarea"
+                type="text" 
+                title="Contents"
+                field={contents}
+                setField={setContents}
+                validateField={validateContents} />
+
+            <Input className={styles.fileInput}
+                inputType="file"
+                type="file"
+                title="Upload image"
+                field={file}
+                setField={setFile}
+                validateField={validateFile}/>
+
+            <RequestButton className={styles.submissionButton}
+                setError={setError} setSubmissionActive={setSubmissionActive}
+                promise={promise} setPromise={setPromise}
+                title={title} setTitle={setTitle}
+                contents={contents} setContents={setContents}
+                file={file} setFile={setFile} subreact={subreact}>
+
+                Submit
+            </RequestButton>
+        </form>
+    );
+}
+
+export default function Submission({active, setActive}) {
+    const [error, setError] = useState(null);
+
+    const location = useLocation();
+    const subreact = getSubreactFromLocation(location);
+
+    const [user] = useContext(UserContext);
+
+    const [state, setState] = useContext(StateContext);
+    const setLoginActive = (active) => {setState({...state, loginActive: active})};
+
+    // If our subreact is the front page, disable our ability to post. Posting
+    // should be done to the main subreact.
+    useEffect(() => {
+        if (subreact === "") {
+            setActive(false);
+        }
+    }, [location]);
+
+    return (
+        <div className={styles.container}>
+            <Card className={styles.card + (active ? " " + styles.cardActive : "")}>
+                <Tab submissionActive={active} setSubmissionActive={setActive} user={user} setLoginActive={setLoginActive} />
+                <h1>Make a new submission</h1>
+                <ErrorMessage message={error} />
+                <SubmissionForm setSubmissionActive={setActive} setError={setError} subreact={subreact}/>
+            </Card>
+        </div>
+    );
+}
\ No newline at end of file
-- 
cgit v1.2.3

