about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--Cargo.toml2
-rw-r--r--client/index.html37
-rw-r--r--client/package.json35
-rw-r--r--client/public/android-chrome-192x192.pngbin0 -> 2527 bytes
-rw-r--r--client/public/android-chrome-512x512.pngbin0 -> 5986 bytes
-rw-r--r--client/public/apple-touch-icon.pngbin0 -> 1103 bytes
-rw-r--r--client/public/favicon-16x16.pngbin0 -> 417 bytes
-rw-r--r--client/public/favicon-32x32.pngbin0 -> 591 bytes
-rw-r--r--client/public/favicon.icobin0 -> 15086 bytes
-rw-r--r--client/public/index.html24
-rw-r--r--client/public/manifest.json19
-rw-r--r--client/public/mstile-150x150.pngbin0 -> 1502 bytes
-rw-r--r--client/public/robots.txt3
-rw-r--r--client/public/safari-pinned-tab.svg1
-rw-r--r--client/src/App.css189
-rw-r--r--client/src/App.js25
-rw-r--r--client/src/Components/Button.js11
-rw-r--r--client/src/Components/CopyButton.js24
-rw-r--r--client/src/Components/Form.js33
-rw-r--r--client/src/Components/Loader.js13
-rw-r--r--client/src/Components/Response.js50
-rw-r--r--client/src/Components/ResponseContainer.js12
-rw-r--r--client/src/Components/Title.js12
-rw-r--r--client/src/index.css13
-rw-r--r--client/src/index.js17
-rw-r--r--client/src/serviceWorker.js141
-rw-r--r--client/static/main.css141
-rw-r--r--client/static/main.js91
-rw-r--r--src/main.rs8
-rw-r--r--src/types.rs7
31 files changed, 639 insertions, 277 deletions
diff --git a/.gitignore b/.gitignore
index 212de44..fd1e9d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
-/target
-.DS_Store
\ No newline at end of file
+/target/
+.DS_Store
+build/
+.idea/
+node_modules/
+*.lock
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index 6e44c76..82aa8ab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "shorest"
-version = "0.1.1"
+version = "0.2.0"
 authors = ["Melonai <einebeere@gmail.com>"]
 edition = "2018"
 
diff --git a/client/index.html b/client/index.html
deleted file mode 100644
index 07478ee..0000000
--- a/client/index.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Shorest</title>
-    <link rel="stylesheet" href="static/main.css">
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/validate.js/0.13.1/validate.min.js"></script>
-    <script src="static/main.js"></script>
-</head>
-<body>
-<div class="active" id="main">
-    <div class="title">
-        <a><b>sho.rest</b><br></a>
-        <a>Made with ❤ by <b>Mel</b></a>
-    </div>
-    <form id="form">
-        <div class="input-group" id="form-group">
-            <div class="input-container" id="left" style="border-right: none;">
-                <a class="input-field-text">https://</a>
-                <input class="input-field" id="url" required>
-            </div>
-            <div class="button-container">
-                <input type="submit" value class="button" id="btn">
-            </div>
-        </div>
-    </form>
-    <div id="responses"></div>
-</div>
-<template id="response-template">
-    <div class="response-container">
-        <div class="title response-text"></div>
-        <a class="copy-text"><strong>Copy Link</strong></a>
-    </div>
-</template>
-</body>
-</html>
\ No newline at end of file
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..0650abf
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,35 @@
+{
+  "name": "shorest",
+  "version": "0.1.0",
+  "private": true,
+  "homepage": "/client/",
+  "dependencies": {
+    "axios": "^0.19.2",
+    "clipboard-copy": "^3.1.0",
+    "react": "^16.13.1",
+    "react-dom": "^16.13.1",
+    "react-scripts": "3.4.1",
+    "shortid": "^2.2.15",
+    "validator": "^13.0.0"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": "react-app"
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}
diff --git a/client/public/android-chrome-192x192.png b/client/public/android-chrome-192x192.png
new file mode 100644
index 0000000..501c9fa
--- /dev/null
+++ b/client/public/android-chrome-192x192.png
Binary files differdiff --git a/client/public/android-chrome-512x512.png b/client/public/android-chrome-512x512.png
new file mode 100644
index 0000000..d264939
--- /dev/null
+++ b/client/public/android-chrome-512x512.png
Binary files differdiff --git a/client/public/apple-touch-icon.png b/client/public/apple-touch-icon.png
new file mode 100644
index 0000000..249732c
--- /dev/null
+++ b/client/public/apple-touch-icon.png
Binary files differdiff --git a/client/public/favicon-16x16.png b/client/public/favicon-16x16.png
new file mode 100644
index 0000000..3ceab15
--- /dev/null
+++ b/client/public/favicon-16x16.png
Binary files differdiff --git a/client/public/favicon-32x32.png b/client/public/favicon-32x32.png
new file mode 100644
index 0000000..652c714
--- /dev/null
+++ b/client/public/favicon-32x32.png
Binary files differdiff --git a/client/public/favicon.ico b/client/public/favicon.ico
new file mode 100644
index 0000000..9d96bed
--- /dev/null
+++ b/client/public/favicon.ico
Binary files differdiff --git a/client/public/index.html b/client/public/index.html
new file mode 100644
index 0000000..f8ac79c
--- /dev/null
+++ b/client/public/index.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta
+      name="description"
+      content="A simple URL shortener!"
+    />
+    <link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
+    <link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
+    <link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <link rel="mask-icon" href="%PUBLIC_URL%/safari-pinned-tab.svg" color="#5bbad5" />
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <meta name="msapplication-TileColor" content="#727272" />
+    <meta name="theme-color" content="#ffffff" />
+    <title>sho.rest</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+  </body>
+</html>
diff --git a/client/public/manifest.json b/client/public/manifest.json
new file mode 100644
index 0000000..37e0ec8
--- /dev/null
+++ b/client/public/manifest.json
@@ -0,0 +1,19 @@
+{
+  "name": "sho.rest",
+  "short_name": "sho.rest",
+  "icons": [
+    {
+      "src": "/android-chrome-192x192.png",
+      "sizes": "192x192",
+      "type": "image/png"
+    },
+    {
+      "src": "/android-chrome-512x512.png",
+      "sizes": "512x512",
+      "type": "image/png"
+    }
+  ],
+  "theme_color": "#ffffff",
+  "background_color": "#ffffff",
+  "display": "standalone"
+}
\ No newline at end of file
diff --git a/client/public/mstile-150x150.png b/client/public/mstile-150x150.png
new file mode 100644
index 0000000..acb3438
--- /dev/null
+++ b/client/public/mstile-150x150.png
Binary files differdiff --git a/client/public/robots.txt b/client/public/robots.txt
new file mode 100644
index 0000000..e9e57dc
--- /dev/null
+++ b/client/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/client/public/safari-pinned-tab.svg b/client/public/safari-pinned-tab.svg
new file mode 100644
index 0000000..2b7a7b4
--- /dev/null
+++ b/client/public/safari-pinned-tab.svg
@@ -0,0 +1 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1266.667" height="1266.667" viewBox="0 0 950.000000 950.000000"><path d="M.3 340.2C.6 713.1.1 682.2 6.1 712c10.2 50.8 36.2 101.5 72.1 140.7 48.4 53 112.1 85.8 185.3 95.5 14.7 1.9 408.3 1.9 423 0 63.9-8.5 120.4-34.4 166.2-76.4 53-48.4 85.8-112.1 95.5-185.3 1.9-14.7 1.9-408.3 0-423-9.7-73.2-42.5-136.9-95.5-185.3C813.5 42.3 762.8 16.3 712 6.1 682.2.1 713.1.6 340.2.3L0 0l.3 340.2zM669.5 84.1C773.8 93 857 176.2 865.9 280.5c1.5 16.4 1.5 372.6 0 389C857 773.8 773.8 857 669.5 865.9c-16.4 1.5-372.6 1.5-389 0C176.2 857 93 773.8 84.1 669.5c-.7-7.9-1.1-109.9-1.1-299.3V83h287.3c189.3 0 291.3.4 299.2 1.1z"/><path d="M449.4 267.6c-21.3 2.9-45.7 10.8-61.9 20-28.1 16.1-45.8 38.2-53.3 66.4-2.1 8.1-2.5 11.7-2.5 24.5 0 16.8 1.5 25.8 6.5 38.4 11.2 28.4 36.6 52 75.1 70.1 12.7 5.9 33 13.7 60.2 23 47.9 16.5 63 26.2 69.7 44.8 3 8.4 3 24.6-.1 32.5-5.7 14.6-18.9 24.2-39 28.2-11.9 2.5-41.1 1.7-52.6-1.4-19.6-5.2-34-15.8-40.8-30.2-3.9-8.3-6.7-19.3-6.7-26.8 0-8.9 4-8.1-43.3-8.1h-42l.6 11.2c2.1 37.7 21.3 70.6 54.3 92.9 9.1 6.1 28.9 15.8 39.4 19.3 9.8 3.2 21.4 6.2 32.5 8.2 11.9 2.2 51.5 3.1 65.7 1.5 54.3-6.2 93.4-29.7 110.3-66.4 11.6-25.1 12-61.4.9-88.7-10.9-27.1-35-49.6-71.9-67.5-15.7-7.6-28-12.3-52-20-34-11-53.9-19.7-66.5-29.2-7-5.2-13.3-14.2-15-21.3-1.7-7-.8-18.1 2-25.5 3-7.9 12.8-17.8 22-22.2 12.1-5.8 17.5-6.8 38.5-6.8 17.1 0 19.7.2 26.5 2.3 16.2 5.1 27.7 14.1 33.9 26.6 3.4 7 6.1 17.5 6.1 24.3v4.3h85.1l-.6-7.8c-2.1-26.4-9.8-47-24.6-66.2-20.8-26.9-54.4-44.7-95.2-50.5-12.9-1.9-47.5-1.8-61.3.1z"/></svg>
\ No newline at end of file
diff --git a/client/src/App.css b/client/src/App.css
new file mode 100644
index 0000000..07c14c2
--- /dev/null
+++ b/client/src/App.css
@@ -0,0 +1,189 @@
+.title {
+  padding-left: 30px;
+  margin-bottom: 10px;
+  line-height: 1.42857143;
+  color: #E0E0E0;
+}
+
+.response-text {
+  margin-bottom: 0;
+}
+
+.copy-text {
+  color: #E0E0E0;
+  padding-right: 30px;
+  text-align: right;
+  -webkit-touch-callout: none !important;
+  -webkit-user-select: none !important;
+  -moz-user-select: none !important;
+  -ms-user-select: none !important;
+  user-select: none !important;
+}
+
+.input-group {
+  position: relative;
+  display: table;
+  border-color: #E0E0E0;
+  border-collapse: separate;
+  transition: border-color 1s;
+}
+
+.response-container {
+  display: flex;
+  align-items: center;
+  border: 2px solid #E0E0E0;
+  margin: 15px 30px 0 30px;
+  height: 14vh;
+  justify-content: space-between;
+  border-radius: 0 5vh 5vh 5vh;
+}
+
+.input-field {
+  position: relative;
+  z-index: 2;
+  margin-bottom: 0;
+  text-indent: 0;
+  color: #727272;
+  background-color: #fff;
+  background-image: none;
+  box-sizing: border-box;
+  border: none;
+  outline: none;
+  height: 100%;
+  width: 100%;
+}
+
+.input-field-text {
+  margin-left: 30px;
+  box-sizing: border-box;
+  color: #E0E0E0;
+}
+
+.input-container {
+  z-index: 2;
+  width: 100%;
+  float: left;
+  display: flex;
+  align-items: center;
+  position: relative;
+  padding: 0;
+  border-radius: 5vh 0 0 5vh;
+  border: 2px solid;
+  border-color: inherit;
+  margin: 0;
+  box-sizing: border-box;
+  height: 10vh;
+}
+
+.button-container {
+  display: table-cell;
+  position: relative;
+  font-size: 0;
+  white-space: nowrap;
+  width: 1%;
+  height: 0;
+  vertical-align: middle;
+  border-color: inherit;
+}
+
+.button {
+  z-index: 2;
+  margin-left: -1px;
+  display: inline-block;
+  margin-bottom: 0;
+  font-weight: 400;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: middle;
+  -ms-touch-action: manipulation;
+  touch-action: manipulation;
+  cursor: pointer;
+  background-image: none;
+  background-color: #fff;
+  color: #727272;
+  padding: 4px 5vw;
+  height: 10vh;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  border-radius: 0 5vh 5vh 0;
+  border: 2px solid;
+  border-left-style: none;
+  transition: color 1s;
+  -webkit-appearance: none;
+  -moz-appearance: none;
+  appearance: none;
+}
+
+strong {
+  font-weight: normal;
+  color: #727272;
+}
+
+a {
+  text-decoration: none;
+}
+
+.disabled {
+  border-color: #ffbcbc;
+  color: #ffbcbc;
+}
+
+.border-r-none {
+  border-right: none;
+}
+
+#btn {
+  border-color: inherit;
+}
+
+@-webkit-keyframes scale {
+  0% {
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    opacity: 1; }
+  45% {
+    -webkit-transform: scale(0.1);
+    transform: scale(0.1);
+    opacity: 0.2; }
+  80% {
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    opacity: 1; } }
+
+@keyframes scale {
+  0% {
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    opacity: 1; }
+  45% {
+    -webkit-transform: scale(0.1);
+    transform: scale(0.1);
+    opacity: 0.2; }
+  80% {
+    -webkit-transform: scale(1);
+    transform: scale(1);
+    opacity: 1; } }
+
+.ball-pulse > div:nth-child(1) {
+  -webkit-animation: scale 0.75s -0.24s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
+  animation: scale 0.75s -0.48s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08); }
+
+.ball-pulse > div:nth-child(2) {
+  -webkit-animation: scale 0.75s -0.12s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
+  animation: scale 0.75s -0.36s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08); }
+
+.ball-pulse > div:nth-child(3) {
+  -webkit-animation: scale 0.75s 0s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
+  animation: scale 0.75s -0.24s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08); }
+
+.ball-pulse > div {
+  background-color: #727272;
+  width: 6px;
+  height: 6px;
+  margin: 5px;
+  border-radius: 100%;
+  -webkit-animation-fill-mode: both;
+  animation-fill-mode: both;
+  display: inline-block; }
\ No newline at end of file
diff --git a/client/src/App.js b/client/src/App.js
new file mode 100644
index 0000000..135a037
--- /dev/null
+++ b/client/src/App.js
@@ -0,0 +1,25 @@
+import React, {useState} from 'react';
+import './App.css';
+import Title from './Components/Title';
+import Form from './Components/Form';
+import ResponseContainer from './Components/ResponseContainer';
+import shortid from 'shortid';
+
+function App() {
+    const [requests, setRequests] = useState([]);
+
+    const addRequest = (newRequest) => {
+        const newRequests = [{url: newRequest, key: shortid.generate()}, ...requests];
+        setRequests(newRequests.slice(0, 2));
+    }
+
+    return (
+        <div>
+            <Title/>
+            <Form addRequest={addRequest}/>
+            <ResponseContainer requests={requests}/>
+        </div>
+    );
+}
+
+export default App;
diff --git a/client/src/Components/Button.js b/client/src/Components/Button.js
new file mode 100644
index 0000000..46c0c27
--- /dev/null
+++ b/client/src/Components/Button.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+function Button(props) {
+    return (
+        <div className="button-container">
+            <input type="submit" value={props.valid ? "→" : ""} className="button" id="btn" onClick={props.submit}/>
+        </div>
+    )
+}
+
+export default Button;
\ No newline at end of file
diff --git a/client/src/Components/CopyButton.js b/client/src/Components/CopyButton.js
new file mode 100644
index 0000000..0ae8e83
--- /dev/null
+++ b/client/src/Components/CopyButton.js
@@ -0,0 +1,24 @@
+import React, {useState} from 'react';
+import copy from 'clipboard-copy';
+
+function CopyButton(props) {
+    const [copied, setCopied] = useState(false);
+
+    const handleClick = async () => {
+        await copy("https://sho.rest/" + props.hash);
+        setCopied(true);
+    };
+
+    let content;
+    if (copied) {
+        content = <span>Link Copied!</span>;
+    } else {
+        content = <strong>Copy Link</strong>;
+    }
+
+    return (
+        <span className="copy-text" onClick={handleClick}>{content}</span>
+    )
+}
+
+export default CopyButton;
\ No newline at end of file
diff --git a/client/src/Components/Form.js b/client/src/Components/Form.js
new file mode 100644
index 0000000..4d10f98
--- /dev/null
+++ b/client/src/Components/Form.js
@@ -0,0 +1,33 @@
+import React, {useState} from 'react';
+import Button from './Button';
+import isURL from "validator/lib/isURL";
+
+function Form(props) {
+    const [state, setState] = useState({value: '', valid: false});
+
+    const handleSubmit = () => {
+        if (state.valid) {
+            props.addRequest(state.value);
+        }
+    };
+
+    const handleChange = (e) => {
+        const userInput = e.target.value;
+        const valid = isURL('https://' + userInput);
+        setState({value: userInput, valid: valid});
+    };
+
+    return (
+        <form id="form" onSubmit={(e) => e.preventDefault()}>
+            <div className={"input-group" + (state.valid ? "" : " disabled")}>
+                <div className={"input-container" + (state.valid ? "" : " border-r-none")}>
+                    <span className="input-field-text">https://</span>
+                    <input className="input-field" required onChange={handleChange}/>
+                </div>
+                <Button valid={state.valid} submit={handleSubmit}/>
+            </div>
+        </form>
+    )
+}
+
+export default Form;
\ No newline at end of file
diff --git a/client/src/Components/Loader.js b/client/src/Components/Loader.js
new file mode 100644
index 0000000..3f4c47f
--- /dev/null
+++ b/client/src/Components/Loader.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+function Loader() {
+    return (
+        <div className="ball-pulse">
+            <div/>
+            <div/>
+            <div/>
+        </div>
+    )
+}
+
+export default Loader;
\ No newline at end of file
diff --git a/client/src/Components/Response.js b/client/src/Components/Response.js
new file mode 100644
index 0000000..83c6ff1
--- /dev/null
+++ b/client/src/Components/Response.js
@@ -0,0 +1,50 @@
+import React, {useEffect, useState} from 'react';
+import axios from "axios";
+import Loader from "./Loader";
+import CopyButton from "./CopyButton";
+
+function Response(props){
+    const CancelToken = axios.CancelToken;
+    const [requestState, setRequestState] = useState({loading: true, cancel: CancelToken.source()});
+
+    useEffect(() => {
+        axios.post('/', {url: "https://" + props.url}, {cancelToken: requestState.cancel.token})
+            .then((r) => {
+                setRequestState({loading: false, hash: r.data.hash, cancel: requestState.cancel});
+            }).catch((e) => {
+                if (!axios.isCancel(e)) {
+                    setRequestState({loading: false, error: true, cancel: requestState.cancel});
+                }
+            });
+
+        return () => {
+            requestState.cancel.cancel();
+        };
+    }, [props.url, requestState.cancel])
+
+    let text;
+    if (!requestState.loading) {
+        if (!requestState.error) {
+            if (props.url.length < 20) {
+                text =
+                    <span>The short link for <strong>{props.url}</strong> is<br/><strong>sho.rest/{requestState.hash}</strong></span>;
+            } else {
+                text =
+                    <span>The short link for your URL is<br/><strong>sho.rest/{requestState.hash}</strong></span>;
+            }
+        } else {
+            text = <span>There was an error.</span>
+        }
+    } else {
+        text = <Loader/>
+    }
+
+    return (
+        <div className={"response-container" + (requestState.error ? " disabled" : "")}>
+            <div className={"title response-text" + (requestState.error ? " disabled" : "")}>{text}</div>
+            {requestState.error || requestState.loading ? "" : <CopyButton hash={requestState.hash}/>}
+        </div>
+    )
+}
+
+export default Response;
\ No newline at end of file
diff --git a/client/src/Components/ResponseContainer.js b/client/src/Components/ResponseContainer.js
new file mode 100644
index 0000000..8fad4bd
--- /dev/null
+++ b/client/src/Components/ResponseContainer.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import Response from "./Response";
+
+function ResponseContainer(props){
+    const responseContent = props.requests.map((r) => <Response key={r.key} url={r.url}/>);
+
+    return (
+        responseContent
+    )
+}
+
+export default ResponseContainer;
\ No newline at end of file
diff --git a/client/src/Components/Title.js b/client/src/Components/Title.js
new file mode 100644
index 0000000..8ea96a9
--- /dev/null
+++ b/client/src/Components/Title.js
@@ -0,0 +1,12 @@
+import React from 'react';
+
+function Title() {
+    return (
+        <div className="title">
+            <span><b>sho.rest</b><br/></span>
+            <span>Made with ❤ by <b>Mel</b></span>
+        </div>
+    )
+}
+
+export default Title;
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
new file mode 100644
index 0000000..d007edc
--- /dev/null
+++ b/client/src/index.css
@@ -0,0 +1,13 @@
+html * {
+  font-family: Roboto, sans-serif;
+  font-size: 3vmin;
+}
+
+#root {
+  justify-content: center;
+  position: absolute;
+  width: 100%;
+  box-sizing: border-box;
+  top: 40%;
+  padding: 0 50px 0 50px;
+}
\ No newline at end of file
diff --git a/client/src/index.js b/client/src/index.js
new file mode 100644
index 0000000..f5185c1
--- /dev/null
+++ b/client/src/index.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import * as serviceWorker from './serviceWorker';
+
+ReactDOM.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+  document.getElementById('root')
+);
+
+// If you want your app to work offline and load faster, you can change
+// unregister() to register() below. Note this comes with some pitfalls.
+// Learn more about service workers: https://bit.ly/CRA-PWA
+serviceWorker.unregister();
diff --git a/client/src/serviceWorker.js b/client/src/serviceWorker.js
new file mode 100644
index 0000000..b04b771
--- /dev/null
+++ b/client/src/serviceWorker.js
@@ -0,0 +1,141 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+  window.location.hostname === 'localhost' ||
+    // [::1] is the IPv6 localhost address.
+    window.location.hostname === '[::1]' ||
+    // 127.0.0.0/8 are considered localhost for IPv4.
+    window.location.hostname.match(
+      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+    )
+);
+
+export function register(config) {
+  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+    // The URL constructor is available in all browsers that support SW.
+    const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+    if (publicUrl.origin !== window.location.origin) {
+      // Our service worker won't work if PUBLIC_URL is on a different origin
+      // from what our page is served on. This might happen if a CDN is used to
+      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+      return;
+    }
+
+    window.addEventListener('load', () => {
+      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+      if (isLocalhost) {
+        // This is running on localhost. Let's check if a service worker still exists or not.
+        checkValidServiceWorker(swUrl, config);
+
+        // Add some additional logging to localhost, pointing developers to the
+        // service worker/PWA documentation.
+        navigator.serviceWorker.ready.then(() => {
+          console.log(
+            'This web app is being served cache-first by a service ' +
+              'worker. To learn more, visit https://bit.ly/CRA-PWA'
+          );
+        });
+      } else {
+        // Is not localhost. Just register service worker
+        registerValidSW(swUrl, config);
+      }
+    });
+  }
+}
+
+function registerValidSW(swUrl, config) {
+  navigator.serviceWorker
+    .register(swUrl)
+    .then(registration => {
+      registration.onupdatefound = () => {
+        const installingWorker = registration.installing;
+        if (installingWorker == null) {
+          return;
+        }
+        installingWorker.onstatechange = () => {
+          if (installingWorker.state === 'installed') {
+            if (navigator.serviceWorker.controller) {
+              // At this point, the updated precached content has been fetched,
+              // but the previous service worker will still serve the older
+              // content until all client tabs are closed.
+              console.log(
+                'New content is available and will be used when all ' +
+                  'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
+              );
+
+              // Execute callback
+              if (config && config.onUpdate) {
+                config.onUpdate(registration);
+              }
+            } else {
+              // At this point, everything has been precached.
+              // It's the perfect time to display a
+              // "Content is cached for offline use." message.
+              console.log('Content is cached for offline use.');
+
+              // Execute callback
+              if (config && config.onSuccess) {
+                config.onSuccess(registration);
+              }
+            }
+          }
+        };
+      };
+    })
+    .catch(error => {
+      console.error('Error during service worker registration:', error);
+    });
+}
+
+function checkValidServiceWorker(swUrl, config) {
+  // Check if the service worker can be found. If it can't reload the page.
+  fetch(swUrl, {
+    headers: { 'Service-Worker': 'script' },
+  })
+    .then(response => {
+      // Ensure service worker exists, and that we really are getting a JS file.
+      const contentType = response.headers.get('content-type');
+      if (
+        response.status === 404 ||
+        (contentType != null && contentType.indexOf('javascript') === -1)
+      ) {
+        // No service worker found. Probably a different app. Reload the page.
+        navigator.serviceWorker.ready.then(registration => {
+          registration.unregister().then(() => {
+            window.location.reload();
+          });
+        });
+      } else {
+        // Service worker found. Proceed as normal.
+        registerValidSW(swUrl, config);
+      }
+    })
+    .catch(() => {
+      console.log(
+        'No internet connection found. App is running in offline mode.'
+      );
+    });
+}
+
+export function unregister() {
+  if ('serviceWorker' in navigator) {
+    navigator.serviceWorker.ready
+      .then(registration => {
+        registration.unregister();
+      })
+      .catch(error => {
+        console.error(error.message);
+      });
+  }
+}
diff --git a/client/static/main.css b/client/static/main.css
deleted file mode 100644
index e24db3a..0000000
--- a/client/static/main.css
+++ /dev/null
@@ -1,141 +0,0 @@
-html * {
-    font-family: Roboto, sans-serif;
-    font-size: 3vmin;
-}
-
-.active {
-    justify-content: center;
-    position: absolute;
-    width: 100%;
-    box-sizing: border-box;
-    top: 40%;
-    padding: 0 50px 0 50px;
-}
-
-.title {
-    padding-left: 30px;
-    margin-bottom: 10px;
-    line-height: 1.42857143;
-    color: #E0E0E0;
-}
-
-.response-text {
-    margin-bottom: 0;
-}
-
-.copy-text {
-    color: #E0E0E0;
-    padding-right: 30px;
-    text-align: right;
-    -webkit-touch-callout: none !important;
-    -webkit-user-select: none !important;
-    -moz-user-select: none !important;
-    -ms-user-select: none !important;
-    user-select: none !important;
-}
-
-.input-group {
-    position: relative;
-    display: table;
-    border-color: #E0E0E0;
-    border-collapse: separate;
-    transition: border-color 1s;
-}
-
-.response-container {
-    display: flex;
-    align-items: center;
-    border: 2px solid #E0E0E0;
-    margin: 15px 30px 0 30px;
-    height: 14vh;
-    justify-content: space-between;
-    border-radius: 0 5vh 5vh 5vh;
-}
-
-.input-field {
-    position: relative;
-    z-index: 2;
-    margin-bottom: 0;
-    text-indent: 0;
-    color: #727272;
-    background-color: #fff;
-    background-image: none;
-    box-sizing: border-box;
-    border: none;
-    outline: none;
-    height: 100%;
-    width: 100%;
-}
-
-.input-field-text {
-    margin-left: 30px;
-    box-sizing: border-box;
-    color: #E0E0E0;
-}
-
-.input-container {
-    z-index: 2;
-    width: 100%;
-    float: left;
-    display: flex;
-    align-items: center;
-    position: relative;
-    padding: 0;
-    border-radius: 5vh 0 0 5vh;
-    border: 2px solid;
-    border-color: inherit;
-    margin: 0;
-    box-sizing: border-box;
-    height: 10vh;
-}
-
-.button-container {
-    display: table-cell;
-    position: relative;
-    font-size: 0;
-    white-space: nowrap;
-    width: 1%;
-    height: 0;
-    vertical-align: middle;
-    border-color: inherit;
-}
-
-.button {
-    z-index: 2;
-    margin-left: -1px;
-    display: inline-block;
-    margin-bottom: 0;
-    font-weight: 400;
-    text-align: center;
-    white-space: nowrap;
-    vertical-align: middle;
-    -ms-touch-action: manipulation;
-    touch-action: manipulation;
-    cursor: pointer;
-    background-image: none;
-    background-color: #fff;
-    color: #727272;
-    padding: 4px 5vw;
-    height: 10vh;
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    -ms-user-select: none;
-    user-select: none;
-    border-radius: 0 5vh 5vh 0;
-    border: 2px solid;
-    border-color: inherit;
-    border-left: none;
-    transition: color 1s;
-    -webkit-appearance: none;
-    -moz-appearance: none;
-    appearance: none;
-}
-
-strong {
-    font-weight: normal;
-    color: #727272;
-}
-
-a {
-    text-decoration: none;
-}
\ No newline at end of file
diff --git a/client/static/main.js b/client/static/main.js
deleted file mode 100644
index 5c8f452..0000000
--- a/client/static/main.js
+++ /dev/null
@@ -1,91 +0,0 @@
-$(document).ready(function() {
-    const FORM = $('#form');
-    const URL_FIELD = $('#url');
-
-    FORM.on('submit', onFormSubmit);
-    FORM.attr("novalidate",true);
-    URL_FIELD.on({'input': inputUpdate, 'paste': pasteTrim});
-
-    function onFormSubmit() {
-        if (validateURL(URL_FIELD.val())) {
-            const data = JSON.stringify({'url': 'https://' + URL_FIELD.val()});
-            $.ajax('/', {method: 'POST', data: data, contentType: 'application/json'}).then(onSuccess);
-        }
-        return false;
-    }
-
-    function onSuccess(response) {
-        const responseDiv = $('#response-template')[0].content.querySelector('div');
-        const node = document.importNode(responseDiv, true);
-        let text;
-        if (URL_FIELD.val().length < 20 ) {
-            text = 'The short link for <strong>' + URL_FIELD.val() + '</strong> is<br><strong>sho.rest/' + response.hash + '</strong>';
-        } else {
-            text = 'The short link for your URL is<br><strong>sho.rest/' + response.hash + '</strong>';
-        }
-        node.querySelector('.response-text').innerHTML = text;
-        $(node).find('.copy-text').on('click', copyClick);
-        $('#responses')[0].prepend(node);
-    }
-
-    function inputUpdate() {
-        const visible = validateURL(URL_FIELD.val())
-        if (!FORM[0].hasAttribute('disabled') === visible) return;
-
-        const valuesDisabled = {borderColor: '#FFBCBC', borderRight: 'none', buttonValue: '', buttonValueColor: '#FFFFFF'};
-        const valuesEnabled = {borderColor: '#E0E0E0', borderRight: '', buttonValue: '→', buttonValueColor: '#727272'};
-
-        const btn = $('#btn');
-        const left = $('#left');
-        const formGroup = $('#form-group');
-
-        let values;
-        if (visible) {
-            values = valuesEnabled;
-            FORM.removeAttr('disabled');
-        } else {
-            values = valuesDisabled;
-            FORM[0].setAttribute('disabled', '');
-        }
-
-        formGroup.css('border-color', values.borderColor);
-        left.css('border-right', values.borderRight);
-        btn.css('color', values.buttonValueColor);
-        btn.val(values.buttonValue);
-    }
-
-    function pasteTrim() {
-        const pattern = /^https?:\/\//;
-        setTimeout(() => {
-            URL_FIELD.val(URL_FIELD.val().replace(pattern, ''));
-            inputUpdate();
-        }, 0);
-    }
-
-    inputUpdate();
-});
-
-function copyClick(event) {
-    const target = $(event.target);
-    if (target.hasClass('copied')) return;
-    const copyText = target.closest('.copy-text');
-    const previousCopied = $('.copied');
-
-    previousCopied.removeClass('copied');
-    previousCopied.html('<strong>Copy Link</strong>');
-    copyText.html('Link Copied!');
-    copyText.addClass('copied');
-
-    const link = copyText.parent().find('.response-text strong').last();
-
-    const range = document.createRange();
-    range.selectNode(link[0]);
-    window.getSelection().removeAllRanges();
-    window.getSelection().addRange(range);
-    document.execCommand('copy');
-    window.getSelection().removeAllRanges();
-}
-
-function validateURL(url) {
-    return !validate({website: 'https://' + url}, {website: {url: true}});
-}
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 72f1cad..6fb0073 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -69,14 +69,14 @@ fn add_to_database_safely(mut hash: String, user_url: String, connection: &PgCon
 }
 
 async fn root(req: HttpRequest) -> HttpResponse {
-    NamedFile::open("./client/index.html").unwrap().into_response(&req).unwrap()
+    NamedFile::open("./client/build/index.html").unwrap().into_response(&req).unwrap()
 }
 
 async fn shorten(params: Json<UserData>, state: Data<PoolState>) -> HttpResponse {
     let user_url = match make_url(&params.url) {
         Ok(parse_result) => parse_result,
         Err(_) => {
-            return HttpResponse::BadRequest().body("The URL you entered does not follow the proper URL format.");
+            return HttpResponse::BadRequest().json(ErrorResponse{error: "The URL you entered does not follow the proper URL format.".to_string()});
         },
     };
     let hash = add_to_database_safely(get_hash_from_string(&user_url), user_url, &state.get().expect("Could not get a connection from pool"));
@@ -87,7 +87,7 @@ async fn shorten(params: Json<UserData>, state: Data<PoolState>) -> HttpResponse
 async fn redirect(info: Path<String>, state: Data<PoolState>) -> HttpResponse {
     match get_url_from_database(&info, &state.get().expect("Could not get a connection from pool")) {
         Ok(url) => HttpResponse::TemporaryRedirect().header("Location", url).finish(),
-        Err(_) => HttpResponse::NotFound().body("The URL you specified could not be found.")
+        Err(_) => HttpResponse::TemporaryRedirect().header("Location", "/").finish()
     }
 }
 
@@ -117,7 +117,7 @@ async fn main() -> std::io::Result<()> {
                     .route(web::get().to(redirect))
             )
             .service(
-                Files::new("/static/", "./client/static/")
+                Files::new("/client/", "./client/build/")
             )
     })
         .bind(("0.0.0.0", port))?
diff --git a/src/types.rs b/src/types.rs
index d068043..0a4926e 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -9,7 +9,12 @@ pub struct UserData {
 
 #[derive(Debug, Serialize)]
 pub struct UserResponse {
-    pub hash: String,
+    pub hash: String
+}
+
+#[derive(Debug, Serialize)]
+pub struct ErrorResponse {
+    pub error: String
 }
 
 #[derive(Queryable, Insertable)]