diff options
| author | Melonai <einebeere@gmail.com> | 2021-01-20 23:18:09 +0100 |
|---|---|---|
| committer | Melonai <einebeere@gmail.com> | 2021-01-20 23:18:09 +0100 |
| commit | 826c7c47785ee01d2b9267919132ada696425344 (patch) | |
| tree | 901cc90be9a953a6c3f968b6c1abe33cc13774b4 /client/src | |
| parent | 2953dec527cedaabaa5f0eb48637c5ddd4a4103b (diff) | |
| download | shorest-826c7c47785ee01d2b9267919132ada696425344.tar.zst shorest-826c7c47785ee01d2b9267919132ada696425344.zip | |
Remade the client in SvelteKit
Diffstat (limited to 'client/src')
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); + }; +} |
