aboutsummaryrefslogtreecommitdiff
path: root/client/src
diff options
context:
space:
mode:
authorMelonai <einebeere@gmail.com>2021-01-20 23:18:09 +0100
committerMelonai <einebeere@gmail.com>2021-01-20 23:18:09 +0100
commit826c7c47785ee01d2b9267919132ada696425344 (patch)
tree901cc90be9a953a6c3f968b6c1abe33cc13774b4 /client/src
parent2953dec527cedaabaa5f0eb48637c5ddd4a4103b (diff)
downloadshorest-826c7c47785ee01d2b9267919132ada696425344.tar.zst
shorest-826c7c47785ee01d2b9267919132ada696425344.zip
Remade the client in SvelteKit
Diffstat (limited to 'client/src')
-rw-r--r--client/src/App.css179
-rw-r--r--client/src/App.js25
-rw-r--r--client/src/Components/Button.js15
-rw-r--r--client/src/Components/CopyButton.js24
-rw-r--r--client/src/Components/Form.js39
-rw-r--r--client/src/Components/Form.svelte64
-rw-r--r--client/src/Components/Loader.js13
-rw-r--r--client/src/Components/Response.js55
-rw-r--r--client/src/Components/Response.svelte33
-rw-r--r--client/src/Components/ResponseContainer.js12
-rw-r--r--client/src/Components/Responses.svelte24
-rw-r--r--client/src/Components/Title.js12
-rw-r--r--client/src/Components/Title.svelte22
-rw-r--r--client/src/Components/icons/ArrowIcon.svelte10
-rw-r--r--client/src/Components/icons/CrossIcon.svelte10
-rw-r--r--client/src/actions/shorten.ts50
-rw-r--r--client/src/app.html19
-rw-r--r--client/src/data/links.ts17
-rw-r--r--client/src/globals.d.ts39
-rw-r--r--client/src/index.css18
-rw-r--r--client/src/index.js17
-rw-r--r--client/src/routes/index.svelte44
-rw-r--r--client/src/serviceWorker.js141
-rw-r--r--client/src/utils/addProtocol.ts6
-rw-r--r--client/src/utils/checkUrl.ts10
-rw-r--r--client/src/utils/debounce.ts12
26 files changed, 360 insertions, 550 deletions
diff --git a/client/src/App.css b/client/src/App.css
deleted file mode 100644
index cf0e529..0000000
--- a/client/src/App.css
+++ /dev/null
@@ -1,179 +0,0 @@
-.title {
- padding-left: 30px;
- margin-bottom: 10px;
- line-height: 1.42857143;
- color: #ffffff;
-}
-
-.response-text {
- margin-bottom: 0;
- color: #C5C5D3;
-}
-
-.right-item {
- color: #ffffff;
- padding-right: 30px;
- text-align: right;
-}
-
-.copy-text {
- color: #c5c5d3;
- -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-collapse: separate;
-}
-
-.response-container {
- display: flex;
- align-items: center;
- margin: 15px 30px 0 30px;
- height: 14vh;
- justify-content: space-between;
- border-radius: 0 5vh 5vh 5vh;
- background-color: #ffffff;
-}
-
-.input-field {
- position: relative;
- z-index: 2;
- margin-bottom: 0;
- text-indent: 0;
- color: #3A3A5C;
- 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: #C5C5D3;
-}
-
-.input-container {
- z-index: 2;
- width: 100%;
- float: left;
- display: flex;
- align-items: center;
- position: relative;
- padding: 0;
- border-radius: 5vh 0 0 5vh;
- margin: 0;
- box-sizing: border-box;
- height: 10vh;
- background-color: white;
-}
-
-.button-container {
- display: table-cell;
- position: relative;
- font-size: 0;
- white-space: nowrap;
- width: 1%;
- height: 0;
- vertical-align: middle;
-}
-
-.button {
- z-index: 2;
- 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: #3A3A5C;
- 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;
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
- border: none;
-}
-
-strong {
- font-weight: normal;
- color: #3A3A5C;
-}
-
-a {
- text-decoration: none;
-}
-
-.disabled {
- background-color: #ff1a61;
- color: #ffffff;
-}
-
-@-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: #3A3A5C;
- 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
deleted file mode 100644
index 135a037..0000000
--- a/client/src/App.js
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index b6ed91b..0000000
--- a/client/src/Components/Button.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {faArrowRight, faTimes} from '@fortawesome/free-solid-svg-icons';
-
-function Button(props) {
- return (
- <div className="button-container">
- <button type="submit" className={"button" + (props.valid ? "" : " disabled")} id="btn" onClick={props.submit}>
- <FontAwesomeIcon icon={props.valid ? faArrowRight : faTimes}/>
- </button>
- </div>
- )
-}
-
-export default Button; \ No newline at end of file
diff --git a/client/src/Components/CopyButton.js b/client/src/Components/CopyButton.js
deleted file mode 100644
index 65c4cb9..0000000
--- a/client/src/Components/CopyButton.js
+++ /dev/null
@@ -1,24 +0,0 @@
-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 right-item" 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
deleted file mode 100644
index 979d9c9..0000000
--- a/client/src/Components/Form.js
+++ /dev/null
@@ -1,39 +0,0 @@
-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});
- };
-
- const handlePaste = e => {
- e.preventDefault();
- const pattern = /^https?:\/\//;
- setState({value: e.clipboardData.getData('Text').replace(pattern, ''), valid: false});
- };
-
- return (
- <form id="form" onSubmit={(e) => e.preventDefault()}>
- <div className="input-group">
- <div className="input-container">
- <span className="input-field-text">https://</span>
- <input className="input-field" required value={state.value} onChange={handleChange} onPaste={handlePaste}/>
- </div>
- <Button valid={state.valid} submit={handleSubmit}/>
- </div>
- </form>
- )
-}
-
-export default Form; \ No newline at end of file
diff --git a/client/src/Components/Form.svelte b/client/src/Components/Form.svelte
new file mode 100644
index 0000000..327d25d
--- /dev/null
+++ b/client/src/Components/Form.svelte
@@ -0,0 +1,64 @@
+<script lang="ts">
+ import shorten from "$actions/shorten";
+ import { links } from "$data/links";
+ import checkUrl from "$utils/checkUrl";
+ import debounce from "$utils/debounce";
+ import ArrowIcon from "./icons/ArrowIcon.svelte";
+ import CrossIcon from "./icons/CrossIcon.svelte";
+
+ let value = "";
+ let valid = false;
+
+ function submit() {
+ const url = checkUrl(value);
+ if (url !== null) {
+ links.add(shorten(url));
+ }
+ }
+
+ const check = debounce(() => valid = !!checkUrl(value), 100);
+
+ // @ts-ignore: Value is a dependency
+ $: value, check();
+</script>
+
+<style>
+ form {
+ position: relative;
+ border: 1px solid #aaaabb;
+ box-shadow: 0 4px 6px #aaaabb30;
+ box-sizing: border-box;
+ border-radius: 5px;
+ }
+
+ .field {
+ box-sizing: border-box;
+ width: 100%;
+ border: none;
+ padding: 15px 50px 15px 20px;
+ background: transparent;
+ font-size: 1rem;
+ }
+
+ .button {
+ position: absolute;
+ right: 10px;
+ margin: auto;
+ top: 0;
+ bottom: 0;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ }
+</style>
+
+<form on:submit|preventDefault={submit}>
+ <input class="field" bind:value type="text"/>
+ <button class="button" type="submit">
+ {#if valid}
+ <ArrowIcon/>
+ {:else}
+ <CrossIcon/>
+ {/if}
+ </button>
+</form>
diff --git a/client/src/Components/Loader.js b/client/src/Components/Loader.js
deleted file mode 100644
index 3f4c47f..0000000
--- a/client/src/Components/Loader.js
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index f69000a..0000000
--- a/client/src/Components/Response.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import React, {useEffect, useState} from 'react';
-import axios from "axios";
-import Loader from "./Loader";
-import CopyButton from "./CopyButton";
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faBomb } from '@fortawesome/free-solid-svg-icons';
-
-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;
- let rightItem;
- if (!requestState.loading) {
- if (!requestState.error) {
- rightItem = <CopyButton hash={requestState.hash}/>;
- 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 {
- rightItem = <FontAwesomeIcon className="right-item" icon={faBomb}/>;
- 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>
- {rightItem}
- </div>
- )
-}
-
-export default Response; \ No newline at end of file
diff --git a/client/src/Components/Response.svelte b/client/src/Components/Response.svelte
new file mode 100644
index 0000000..215af1b
--- /dev/null
+++ b/client/src/Components/Response.svelte
@@ -0,0 +1,33 @@
+<script lang="ts">
+ import type { ShortenRequest } from "$actions/shorten";
+
+ export let info: ShortenRequest;
+</script>
+
+<style>
+ div {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .url {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .output {
+ margin-left: 50px;
+ }
+</style>
+
+<div>
+ <span class="url">{info.url}</span>
+ {#await info.response}
+ <span class="output">Loading...</span>
+ {:then { hash }}
+ <a class="output" href="https://sho.rest/{hash}">sho.rest/{hash}</a>
+ {:catch { error }}
+ <span class="output">{error}</span>
+ {/await}
+</div> \ No newline at end of file
diff --git a/client/src/Components/ResponseContainer.js b/client/src/Components/ResponseContainer.js
deleted file mode 100644
index 8fad4bd..0000000
--- a/client/src/Components/ResponseContainer.js
+++ /dev/null
@@ -1,12 +0,0 @@
-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/Responses.svelte b/client/src/Components/Responses.svelte
new file mode 100644
index 0000000..7124f1e
--- /dev/null
+++ b/client/src/Components/Responses.svelte
@@ -0,0 +1,24 @@
+<script lang="ts">
+ import Response from "./Response.svelte"
+ import { slide } from 'svelte/transition';
+ import { links } from "$data/links";
+</script>
+
+<style>
+ ul {
+ list-style: none;
+ padding: 0 10px;
+ }
+
+ li {
+ margin-bottom: 10px;
+ }
+</style>
+
+<ul>
+ {#each $links as info (info.nonce)}
+ <li transition:slide >
+ <Response {info}/>
+ </li>
+ {/each}
+</ul> \ No newline at end of file
diff --git a/client/src/Components/Title.js b/client/src/Components/Title.js
deleted file mode 100644
index 8ea96a9..0000000
--- a/client/src/Components/Title.js
+++ /dev/null
@@ -1,12 +0,0 @@
-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/Components/Title.svelte b/client/src/Components/Title.svelte
new file mode 100644
index 0000000..4266eb1
--- /dev/null
+++ b/client/src/Components/Title.svelte
@@ -0,0 +1,22 @@
+<style>
+ p {
+ color: #212121;
+ margin: 0 0 5px 0;
+ }
+
+ .text {
+ margin: 10px 0 15px 0;
+ }
+
+ img {
+ width: 25px;
+ }
+</style>
+
+<div>
+ <img src="/shorest.svg" alt=""/>
+ <div class="text">
+ <p><b>sho.rest</b></p>
+ <p>Made with ❤ by <b>Mel</b></p>
+ </div>
+</div>
diff --git a/client/src/Components/icons/ArrowIcon.svelte b/client/src/Components/icons/ArrowIcon.svelte
new file mode 100644
index 0000000..52c79ae
--- /dev/null
+++ b/client/src/Components/icons/ArrowIcon.svelte
@@ -0,0 +1,10 @@
+<style>
+ svg {
+ width: 20px;
+ color: #212121;
+ }
+</style>
+
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
+</svg> \ No newline at end of file
diff --git a/client/src/Components/icons/CrossIcon.svelte b/client/src/Components/icons/CrossIcon.svelte
new file mode 100644
index 0000000..55525d8
--- /dev/null
+++ b/client/src/Components/icons/CrossIcon.svelte
@@ -0,0 +1,10 @@
+<style>
+ svg {
+ width: 20px;
+ color: #212121;
+ }
+</style>
+
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
+</svg> \ No newline at end of file
diff --git a/client/src/actions/shorten.ts b/client/src/actions/shorten.ts
new file mode 100644
index 0000000..58dd4f0
--- /dev/null
+++ b/client/src/actions/shorten.ts
@@ -0,0 +1,50 @@
+interface ShortenResponse {
+ hash: string;
+}
+
+export interface ShortenRequest {
+ url: string;
+ nonce: string;
+ response: Promise<ShortenResponse>;
+}
+
+async function makeRequest(url: string): Promise<ShortenResponse> {
+ let body;
+
+ try {
+ const response = await fetch("http://localhost:4000/", {
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ method: "post",
+ body: JSON.stringify({ url }),
+ });
+
+ body = await response.json();
+ } catch (err) {
+ throw {
+ error: "Error!",
+ };
+ }
+
+ if (body.hash) {
+ return {
+ hash: body.hash,
+ };
+ } else {
+ throw {
+ message: body.error || "Error!",
+ };
+ }
+}
+
+export default function shorten(url: string): ShortenRequest {
+ const nonce = Math.random().toString(36).substr(2, 5);
+
+ return {
+ url,
+ nonce,
+ response: makeRequest(url),
+ };
+}
diff --git a/client/src/app.html b/client/src/app.html
new file mode 100644
index 0000000..fe758f5
--- /dev/null
+++ b/client/src/app.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <title>sho.rest</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" href="/favicon.ico" />
+ <link rel="stylesheet" href="/global.css" />
+ <link rel="preconnect" href="https://fonts.gstatic.com" />
+ <link
+ href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap"
+ rel="stylesheet"
+ />
+ %svelte.head%
+ </head>
+ <body>
+ %svelte.body%
+ </body>
+</html>
diff --git a/client/src/data/links.ts b/client/src/data/links.ts
new file mode 100644
index 0000000..3ccf100
--- /dev/null
+++ b/client/src/data/links.ts
@@ -0,0 +1,17 @@
+import type { ShortenRequest } from "$actions/shorten";
+import { Writable, writable } from "svelte/store";
+
+function createLinks() {
+ const { subscribe, update }: Writable<ShortenRequest[]> = writable([]);
+
+ function add(request: ShortenRequest) {
+ update((l) => [request, ...l.slice(0, 2)]);
+ }
+
+ return {
+ subscribe,
+ add,
+ };
+}
+
+export const links = createLinks();
diff --git a/client/src/globals.d.ts b/client/src/globals.d.ts
new file mode 100644
index 0000000..06d88b7
--- /dev/null
+++ b/client/src/globals.d.ts
@@ -0,0 +1,39 @@
+/// <reference types="@sveltejs/kit" />
+
+//#region Ensure Svelte file endings have a type for TypeScript
+declare module '*.svelte' {
+ export { SvelteComponent as default } from 'svelte';
+}
+//#endregion
+
+//#region Ensure image file endings have a type for TypeScript
+declare module "*.gif" {
+ const value: string;
+ export = value;
+}
+
+declare module "*.jpg" {
+ const value: string;
+ export = value;
+}
+
+declare module "*.jpeg" {
+ const value: string;
+ export = value;
+}
+
+declare module "*.png" {
+ const value: string;
+ export = value;
+}
+
+declare module "*.svg" {
+ const value: string;
+ export = value;
+}
+
+declare module "*.webp" {
+ const value: string;
+ export = value;
+}
+//#endregion
diff --git a/client/src/index.css b/client/src/index.css
deleted file mode 100644
index b7be091..0000000
--- a/client/src/index.css
+++ /dev/null
@@ -1,18 +0,0 @@
-html * {
- font-family: 'Ubuntu', sans-serif;
- font-size: 3vmin;
-}
-
-body {
- background-color: #3A3A5C;
- margin: 0;
-}
-
-#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
deleted file mode 100644
index f5185c1..0000000
--- a/client/src/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-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/routes/index.svelte b/client/src/routes/index.svelte
new file mode 100644
index 0000000..5e51f13
--- /dev/null
+++ b/client/src/routes/index.svelte
@@ -0,0 +1,44 @@
+<script lang="ts">
+ import Title from '$components/Title.svelte';
+ import Form from '$components/Form.svelte';
+ import Responses from '$components/Responses.svelte';
+</script>
+
+<style>
+ main {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ overflow: hidden;
+ }
+
+ .output {
+ position: absolute;
+ top: 175px;
+ width: 100%;
+ }
+
+ .content {
+ position: relative;
+ width: 80%;
+ }
+
+ @media (min-width: 800px) {
+ .content {
+ width: 40%;
+ }
+ }
+</style>
+
+<main>
+ <div class="content">
+ <Title/>
+ <Form/>
+ <div class="output">
+ <Responses/>
+ </div>
+ </div>
+</main> \ No newline at end of file
diff --git a/client/src/serviceWorker.js b/client/src/serviceWorker.js
deleted file mode 100644
index b04b771..0000000
--- a/client/src/serviceWorker.js
+++ /dev/null
@@ -1,141 +0,0 @@
-// 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/src/utils/addProtocol.ts b/client/src/utils/addProtocol.ts
new file mode 100644
index 0000000..75c6214
--- /dev/null
+++ b/client/src/utils/addProtocol.ts
@@ -0,0 +1,6 @@
+export default function (url: string) {
+ if (!/^https?:\/\//.test(url)) {
+ url = "https://" + url;
+ }
+ return url;
+}
diff --git a/client/src/utils/checkUrl.ts b/client/src/utils/checkUrl.ts
new file mode 100644
index 0000000..8ed747f
--- /dev/null
+++ b/client/src/utils/checkUrl.ts
@@ -0,0 +1,10 @@
+import addProtocol from "./addProtocol";
+
+export default function (url: string): string | null {
+ try {
+ const normalizedUrl = new URL(addProtocol(url));
+ return normalizedUrl.toString();
+ } catch (e) {
+ return null;
+ }
+} \ No newline at end of file
diff --git a/client/src/utils/debounce.ts b/client/src/utils/debounce.ts
new file mode 100644
index 0000000..86ef3db
--- /dev/null
+++ b/client/src/utils/debounce.ts
@@ -0,0 +1,12 @@
+type Procedure = (...args: any[]) => any;
+
+export default function <F extends Procedure>(f: F, duration: number) {
+ let timeout: ReturnType<typeof setTimeout> | null = null;
+ return function (...args: Parameters<F>) {
+ if (timeout !== null) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ timeout = setTimeout(() => f(...args), duration);
+ };
+}