aboutsummaryrefslogtreecommitdiff
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-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
12 files changed, 540 insertions, 0 deletions
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);
+ });
+ }
+}