diff --git a/assets/js/page_upload.jsx b/assets/js/page_upload.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..071298995c712f3bd5fb855435ffc4cf39999b65
--- /dev/null
+++ b/assets/js/page_upload.jsx
@@ -0,0 +1,49 @@
+function postData(url, data) {
+    return fetch(url, {
+        body: data,
+        cache: 'no-cache',
+        credentials: 'same-origin',
+        method: 'POST',
+        mode: 'cors',
+        redirect: 'follow'
+    }).then(response => response.json())
+}
+
+const form = document.querySelector("form.upload");
+const element = document.querySelector("form.upload input[type=file]");
+const results = document.querySelector(".uploading.images");
+element.addEventListener("change", () => {
+    for (let file of element.files) {
+        const reader = new FileReader();
+        reader.addEventListener("load", (e) => {
+            const dataUrl = e.target.result;
+
+            const node = <div className="uploading image">
+                <h2 className="title fake-input" contentEditable="true" placeholder="Title"/>
+                <a className="image">
+                    <img src={dataUrl}/>
+                </a>
+                <p className="description fake-input" contentEditable="true" placeholder="Description" data-multiline/>
+            </div>;
+            results.appendChild(node);
+
+            const data = new FormData();
+            data.append("file", file, file.name);
+
+            postData("/upload/", data).then((json) => {
+                if (json.success) {
+                    node.querySelector("a.image").href = "/" + json.id;
+                    node.querySelector("a.image img").src = "/" + json.id;
+                } else {
+                    node.insertBefore(
+                        <div className="alert error">{JSON.stringify(json.errors)}</div>,
+                        node.querySelector(".description")
+                    )
+                }
+
+                console.log(json);
+            });
+        });
+        reader.readAsDataURL(file);
+    }
+})
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index ca00badf20f50290fb46822479b5689ccb253398..4128d1d13fa2da543cf3905ac3999fe9af940208 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,21 @@
       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
       "dev": true
     },
+    "acorn": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz",
+      "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-4.1.1.tgz",
+      "integrity": "sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw==",
+      "dev": true,
+      "requires": {
+        "acorn": "5.5.3"
+      }
+    },
     "amdefine": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
@@ -270,6 +285,12 @@
       "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
       "dev": true
     },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
     "delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -307,6 +328,46 @@
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
       "dev": true
     },
+    "escodegen": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz",
+      "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==",
+      "dev": true,
+      "requires": {
+        "esprima": "3.1.3",
+        "estraverse": "4.2.0",
+        "esutils": "2.0.2",
+        "optionator": "0.8.2",
+        "source-map": "0.6.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "dev": true,
+          "optional": true
+        }
+      }
+    },
+    "esprima": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
+      "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
+      "dev": true
+    },
+    "estraverse": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+      "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "dev": true
+    },
     "extend": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@@ -319,6 +380,12 @@
       "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
       "dev": true
     },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
     "find-up": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
@@ -711,6 +778,16 @@
         "invert-kv": "1.0.0"
       }
     },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "1.1.2",
+        "type-check": "0.3.2"
+      }
+    },
     "load-json-file": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
@@ -792,6 +869,12 @@
         "trim-newlines": "1.0.0"
       }
     },
+    "merge": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz",
+      "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=",
+      "dev": true
+    },
     "mime-db": {
       "version": "1.33.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
@@ -845,6 +928,20 @@
       "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
       "dev": true
     },
+    "nativejsx": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/nativejsx/-/nativejsx-4.3.0.tgz",
+      "integrity": "sha512-tzLpApz/e1T8mAJg0wUba25vFr6TEoxwcc1d7xxhwrfcRDUHVtMRE5+7hKSb2jT4HaUk3iuj9KSAAkJ0O//O8A==",
+      "dev": true,
+      "requires": {
+        "acorn": "5.5.3",
+        "acorn-jsx": "4.1.1",
+        "commander": "2.15.0",
+        "escodegen": "1.9.1",
+        "glob": "7.1.2",
+        "merge": "1.2.0"
+      }
+    },
     "node-gyp": {
       "version": "3.6.2",
       "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.6.2.tgz",
@@ -961,6 +1058,20 @@
         "wrappy": "1.0.2"
       }
     },
+    "optionator": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+      "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+      "dev": true,
+      "requires": {
+        "deep-is": "0.1.3",
+        "fast-levenshtein": "2.0.6",
+        "levn": "0.3.0",
+        "prelude-ls": "1.1.2",
+        "type-check": "0.3.2",
+        "wordwrap": "1.0.0"
+      }
+    },
     "os-homedir": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
@@ -1048,6 +1159,12 @@
         "pinkie": "2.0.4"
       }
     },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
     "process-nextick-args": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
@@ -1427,6 +1544,15 @@
       "dev": true,
       "optional": true
     },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "1.1.2"
+      }
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -1492,6 +1618,12 @@
         "string-width": "1.0.2"
       }
     },
+    "wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+      "dev": true
+    },
     "wrap-ansi": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
diff --git a/package.json b/package.json
index 87979da683f4763913ba84d8de646c5d3b64a717..68cbd5c32c7e39a6769b5b041759d429e65df834 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,11 @@
 {
   "scripts": {
     "sass": "node_modules/node-sass/bin/node-sass --output-style compressed assets/sass -o assets/css",
-    "build": "npm run sass"
+    "jsx": "node_modules/nativejsx/bin/nativejsx assets/js/*.jsx",
+    "build": "npm run sass && npm run jsx"
   },
   "devDependencies": {
+    "nativejsx": "^4.3.0",
     "node-sass": "^4.7.2"
   }
 }
diff --git a/templates/upload.html b/templates/upload.html
index cecd3a852eacae3ce5a6da1a212e0feb4be04fad..9fd4431c2137c303962d2972823f13ac400900ab 100644
--- a/templates/upload.html
+++ b/templates/upload.html
@@ -27,44 +27,5 @@
     </form>
     <div class="uploading images"></div>
 </div>
-<script>
-    function postData(url, data) {
-        return fetch(url, {
-            body: data,
-            cache: 'no-cache',
-            credentials: 'same-origin',
-            method: 'POST',
-            mode: 'cors',
-            redirect: 'follow'
-        }).then(response => response.json())
-    }
-
-    const form = document.querySelector("form.upload");
-    const element = document.querySelector("form.upload input[type=file]");
-    const results = document.querySelector(".uploading.images");
-    element.addEventListener("change", () => {
-        for (let file of element.files) {
-            const reader = new FileReader();
-            reader.addEventListener("load", (e) => {
-                const node = document.createElement("div");
-                node.classList.add("uploading");
-                node.classList.add("image");
-                const img = document.createElement("img");
-                img.src = e.target.result;
-                node.appendChild(img);
-                results.appendChild(node);
-
-                const data = new FormData();
-                data.append("file", file, file.name);
-
-                postData("/upload/", data).then((json) => {
-                    const text = document.createElement("pre");
-                    text.innerText = JSON.stringify(json);
-                    node.appendChild(text);
-                });
-            });
-            reader.readAsDataURL(file);
-        }
-    })
-</script>
+<script src="/assets/js/page_upload.js"></script>
 {{end}}
\ No newline at end of file