UX (#23)
* Move wrapper css to outer #app * Align the header with logo * Solve generic layout styling * Add icon component * remove wrapper padding * Move basic margin to a css var * Remove hemmelig text after logo * Update by using css var * Adjust font sizes * Update home text * Add spinner * Minor adjustments for the copy icon * Add input grouping * Add initial sign in menu button * Add sign in text * Set username length requirement to auth controller * Better input group alignment * Separate header and body wrapper * Update the logo to the new design for manifest * Add new logo * Add new logo * Set correct color for header bg and logo * Remove background pattern, and update the bg color * Remove highlighting links on click
BIN
logo.png
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 15 MiB |
25
logo.svg
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
src/client/assets/dot-grid.png
Normal file
After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
BIN
src/client/assets/icons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/client/assets/icons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 925 B After Width: | Height: | Size: 846 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.3 KiB |
@ -16,14 +16,16 @@ const App = () => (
|
||||
<>
|
||||
<div id="app">
|
||||
<Header />
|
||||
<Router>
|
||||
<Home path="/" />
|
||||
<Secret path="/secret/:secretId" />
|
||||
<SignIn path="/signin" />
|
||||
<Privacy path="/privacy" />
|
||||
<Account path="/account" />
|
||||
<ApiDocs path="/api-docs" />
|
||||
</Router>
|
||||
<div id="app-inner">
|
||||
<Router>
|
||||
<Home path="/" />
|
||||
<Secret path="/secret/:secretId" />
|
||||
<SignIn path="/signin" />
|
||||
<Privacy path="/privacy" />
|
||||
<Account path="/account" />
|
||||
<ApiDocs path="/api-docs" />
|
||||
</Router>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,5 +1,6 @@
|
||||
.footer {
|
||||
height: 25px;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
7
src/client/components/form/input-group.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
const InputGroup = ({ children }) => <div class={style.inputGroup}>{children}</div>;
|
||||
|
||||
export default InputGroup;
|
@ -4,7 +4,7 @@
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-background);
|
||||
border: 1px solid #eee;
|
||||
margin-bottom: 7px;
|
||||
margin-bottom: var(--margin-basic);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
}
|
||||
|
||||
.thick {
|
||||
border: 7px solid #eee;
|
||||
border: var(--margin-basic) solid #eee;
|
||||
}
|
||||
|
||||
.select,
|
||||
@ -24,12 +24,12 @@
|
||||
background-color: var(--color-background);
|
||||
border: 1px solid #eee;
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: var(--margin-basic);
|
||||
}
|
||||
|
||||
.buttonBurn,
|
||||
.buttonCreate {
|
||||
margin: 7px 0;
|
||||
margin: var(--margin-basic) 0;
|
||||
width: 100%;
|
||||
max-width: 150px;
|
||||
height: 50px;
|
||||
@ -62,9 +62,29 @@
|
||||
.buttonBurn {
|
||||
color: var(--color-font);
|
||||
border: 2px solid var(--color-contrast);
|
||||
background-color: #fff;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.full {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.inputGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.inputGroup {
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.inputGroup > select {
|
||||
margin-right: calc(var(--margin-basic) / 2);
|
||||
}
|
||||
|
||||
.inputGroup > input {
|
||||
margin-left: calc(var(--margin-basic) / 2);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { h } from 'preact';
|
||||
import { Link } from 'preact-router/match';
|
||||
import Logo from './logo.js';
|
||||
import { Account } from '../icon';
|
||||
|
||||
import style from './style.css';
|
||||
|
||||
const Header = () => (
|
||||
<header class={style.header}>
|
||||
<Link class={style.link} href="/">
|
||||
<Logo class={style.logo} />
|
||||
</Link>
|
||||
|
||||
<Link class={style.linkButton} href="/signin">
|
||||
<span>Sign in</span> <Account />
|
||||
</Link>
|
||||
</header>
|
||||
);
|
||||
|
||||
|
@ -1,25 +1,64 @@
|
||||
.header {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: #fff;
|
||||
max-width: 1024px;
|
||||
height: auto;
|
||||
padding: 5px 15px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: var(--margin-basic);
|
||||
box-shadow: 0 0 5px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo g {
|
||||
fill: var(--color-contrast);
|
||||
}
|
||||
|
||||
.link,
|
||||
.logo {
|
||||
width: 110px;
|
||||
width: 60px;
|
||||
height: auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: var(--margin-basic) 0;
|
||||
width: 100%;
|
||||
max-width: 110px;
|
||||
padding: 8px;
|
||||
height: 40px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: var(--color-contrast) !important;
|
||||
text-decoration: none;
|
||||
border: 2px solid var(--color-contrast);
|
||||
}
|
||||
|
||||
.linkButton:hover {
|
||||
border: 2px solid var(--color-contrast);
|
||||
}
|
||||
|
||||
.linkButton:active {
|
||||
filter: brightness(95%);
|
||||
}
|
||||
|
||||
.linkButton svg {
|
||||
width: 20px;
|
||||
height: auto;
|
||||
fill: var(--color-contrast);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.link,
|
||||
.logo {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
13
src/client/components/icon/account.js
Normal file
@ -0,0 +1,13 @@
|
||||
const Account = ({ ...rest }) => (
|
||||
<svg
|
||||
{...rest}
|
||||
viewBox="0 0 1408 1536"
|
||||
aria-labelledby="wvsi-awesome-user-title"
|
||||
id="si-awesome-user"
|
||||
>
|
||||
<title id="wvsi-awesome-user-title">icon user</title>
|
||||
<path d="M1408 1277q0 120-73 189.5t-194 69.5H267q-121 0-194-69.5T0 1277q0-53 3.5-103.5t14-109T44 956t43-97.5 62-81 85.5-53.5T346 704q9 0 42 21.5t74.5 48 108 48T704 843t133.5-21.5 108-48 74.5-48 42-21.5q61 0 111.5 20t85.5 53.5 62 81 43 97.5 26.5 108.5 14 109 3.5 103.5zm-320-893q0 159-112.5 271.5T704 768 432.5 655.5 320 384t112.5-271.5T704 0t271.5 112.5T1088 384z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Account;
|
8
src/client/components/icon/copy.js
Normal file
@ -0,0 +1,8 @@
|
||||
const Copy = ({ ...rest }) => (
|
||||
<svg viewBox="0 0 1792 1792" aria-labelledby="glsi-awesome-copy-title" id="si-awesome-copy">
|
||||
<title id="glsi-awesome-copy-title">icon copy</title>
|
||||
<path d="M1696 384q40 0 68 28t28 68v1216q0 40-28 68t-68 28H736q-40 0-68-28t-28-68v-288H96q-40 0-68-28t-28-68V640q0-40 20-88t48-76L476 68q28-28 76-48t88-20h416q40 0 68 28t28 68v328q68-40 128-40h416zm-544 213L853 896h299V597zM512 213L213 512h299V213zm196 647l316-316V128H640v416q0 40-28 68t-68 28H128v640h512v-256q0-40 20-88t48-76zm956 804V512h-384v416q0 40-28 68t-68 28H768v640h896z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Copy;
|
28
src/client/components/icon/icon-button.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { h } from 'preact';
|
||||
import style from './style.css';
|
||||
|
||||
import { Copy, Account } from '.';
|
||||
|
||||
const getIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'copy':
|
||||
return Copy;
|
||||
case 'account':
|
||||
return Account;
|
||||
|
||||
default:
|
||||
return () => {};
|
||||
}
|
||||
};
|
||||
|
||||
const IconButton = ({ icon = '', ...rest }) => {
|
||||
const Icon = getIcon(icon);
|
||||
|
||||
return (
|
||||
<button class={style.button} {...rest}>
|
||||
<Icon />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconButton;
|
4
src/client/components/icon/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as Copy } from './copy';
|
||||
export { default as Account } from './account';
|
||||
|
||||
// More available: https://leungwensen.github.io/svg-icon/#awesome
|
16
src/client/components/icon/style.css
Normal file
@ -0,0 +1,16 @@
|
||||
.button {
|
||||
width: 28px;
|
||||
height: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button svg {
|
||||
align-self: center;
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.button svg:active {
|
||||
fill: #666;
|
||||
}
|
6
src/client/components/spinner/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { h } from 'preact';
|
||||
import style from './style.css';
|
||||
|
||||
const Spinner = () => <div class={style.loader}>Loading...</div>;
|
||||
|
||||
export default Spinner;
|
94
src/client/components/spinner/style.css
Normal file
@ -0,0 +1,94 @@
|
||||
.loader {
|
||||
color: var(--color-dark);
|
||||
opacity: 0.9;
|
||||
font-size: 30px;
|
||||
text-indent: -9999em;
|
||||
overflow: hidden;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 50%;
|
||||
margin: 72px auto;
|
||||
position: relative;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation: load6 1.7s infinite ease, round 1.7s infinite ease;
|
||||
animation: load6 1.7s infinite ease, round 1.7s infinite ease;
|
||||
}
|
||||
|
||||
@-webkit-keyframes load6 {
|
||||
0% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em,
|
||||
0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
5%,
|
||||
95% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em,
|
||||
0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
10%,
|
||||
59% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em,
|
||||
-0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
|
||||
}
|
||||
20% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em,
|
||||
-0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
|
||||
}
|
||||
38% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em,
|
||||
-0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em,
|
||||
0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
}
|
||||
@keyframes load6 {
|
||||
0% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em,
|
||||
0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
5%,
|
||||
95% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em,
|
||||
0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
10%,
|
||||
59% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.087em -0.825em 0 -0.42em, -0.173em -0.812em 0 -0.44em,
|
||||
-0.256em -0.789em 0 -0.46em, -0.297em -0.775em 0 -0.477em;
|
||||
}
|
||||
20% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.338em -0.758em 0 -0.42em, -0.555em -0.617em 0 -0.44em,
|
||||
-0.671em -0.488em 0 -0.46em, -0.749em -0.34em 0 -0.477em;
|
||||
}
|
||||
38% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, -0.377em -0.74em 0 -0.42em, -0.645em -0.522em 0 -0.44em,
|
||||
-0.775em -0.297em 0 -0.46em, -0.82em -0.09em 0 -0.477em;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 -0.83em 0 -0.4em, 0 -0.83em 0 -0.42em, 0 -0.83em 0 -0.44em,
|
||||
0 -0.83em 0 -0.46em, 0 -0.83em 0 -0.477em;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes round {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes round {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
.wrapper {
|
||||
padding: 25px 25px;
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
@ -8,6 +6,4 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "Hemmelig",
|
||||
"short_name": "Hemmelig",
|
||||
"theme_color": "#2a9d8f",
|
||||
"background_color": "#fff",
|
||||
"description": "Paste a password, secret message or private information",
|
||||
"display": "fullscreen",
|
||||
"theme_color": "#deefea",
|
||||
"background_color": "#231e23",
|
||||
"display": "fullScreen",
|
||||
"orientation": "portrait",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
@ -45,7 +44,7 @@
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/icon-512x512.png",
|
||||
"src": "images/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ import { h } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import { getToken, hasToken } from '../../helpers/token';
|
||||
import { Link, route } from 'preact-router';
|
||||
import style from './style.css';
|
||||
|
||||
import Wrapper from '../../components/wrapper';
|
||||
import Input from '../../components/form/input';
|
||||
import Button from '../../components/form/button';
|
||||
import Spinner from '../../components/spinner';
|
||||
|
||||
import Error from '../../components/info/error';
|
||||
import Info from '../../components/info/info';
|
||||
@ -32,7 +32,7 @@ const Account = () => {
|
||||
try {
|
||||
const response = await getUser(token);
|
||||
|
||||
if (response.statusCode === 401) {
|
||||
if (response.statusCode === 401 || response.statusCode === 500) {
|
||||
setError('Not logged in');
|
||||
|
||||
return;
|
||||
@ -57,10 +57,14 @@ const Account = () => {
|
||||
route('/signin', true);
|
||||
};
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (error) {
|
||||
return <Error>{error}</Error>;
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Wrapper>
|
||||
|
@ -1,5 +1,5 @@
|
||||
.code {
|
||||
margin: 7px 0;
|
||||
margin: var(--margin-basic) 0;
|
||||
width: 100%;
|
||||
white-space: pre-wrap;
|
||||
display: block;
|
||||
|
@ -3,6 +3,7 @@ import { useEffect, useState, useRef } from 'preact/hooks';
|
||||
import style from './style.css';
|
||||
|
||||
import Wrapper from '../../components/wrapper';
|
||||
import InputGroup from '../../components/form/input-group';
|
||||
import Input from '../../components/form/input';
|
||||
import Textarea from '../../components/form/textarea';
|
||||
import Select from '../../components/form/select';
|
||||
@ -10,6 +11,8 @@ import Button from '../../components/form/button';
|
||||
import Error from '../../components/info/error';
|
||||
import Info from '../../components/info/info';
|
||||
|
||||
import IconButton from '../../components/icon/icon-button';
|
||||
|
||||
import { createSecret, burnSecret } from '../../api/secret';
|
||||
|
||||
const Home = () => {
|
||||
@ -86,10 +89,8 @@ const Home = () => {
|
||||
<Wrapper>
|
||||
<h1 class={style.h1}>Paste a password, secret message, or private information.</h1>
|
||||
<Info>
|
||||
Keep your sensitive information out of chat logs, emails, SMS, and more.
|
||||
</Info>
|
||||
<Info>
|
||||
<strong>Hemmelig</strong>, [he`m:(ə)li], means secret in Norwegian.
|
||||
Keep your sensitive information out of chat logs, emails, and more with heavily
|
||||
encrypted secrets.
|
||||
</Info>
|
||||
<div class={style.form}>
|
||||
<Textarea
|
||||
@ -100,29 +101,35 @@ const Home = () => {
|
||||
readonly={!!secretId}
|
||||
thickBorder={!!secretId}
|
||||
/>
|
||||
|
||||
<Select value={ttl} onChange={onSelectChange}>
|
||||
<option value="604800">7 days</option>
|
||||
<option value="259200">3 days</option>
|
||||
<option value="86400">1 day</option>
|
||||
<option value="43200">12 hours</option>
|
||||
<option value="14400">4 hours</option>
|
||||
<option value="3600">1 hour</option>
|
||||
<option value="1800">30 minutes</option>
|
||||
<option value="300">5 minutes</option>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
placeholder="Your optional password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={onPasswordChange}
|
||||
readonly={!!secretId}
|
||||
/>
|
||||
|
||||
<InputGroup>
|
||||
<Select value={ttl} onChange={onSelectChange}>
|
||||
<option value="604800">7 days</option>
|
||||
<option value="259200">3 days</option>
|
||||
<option value="86400">1 day</option>
|
||||
<option value="43200">12 hours</option>
|
||||
<option value="14400">4 hours</option>
|
||||
<option value="3600">1 hour</option>
|
||||
<option value="1800">30 minutes</option>
|
||||
<option value="300">5 minutes</option>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder="Your optional password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={onPasswordChange}
|
||||
readonly={!!secretId}
|
||||
/>
|
||||
</InputGroup>
|
||||
{secretId && (
|
||||
<>
|
||||
<p class={style.info}>Share this link:</p>
|
||||
<Info align="left">
|
||||
<IconButton
|
||||
icon="copy"
|
||||
onClick={() => navigator.clipboard.writeText(getSecretURL())}
|
||||
/>
|
||||
Copy and share the secret link
|
||||
</Info>
|
||||
|
||||
<Input
|
||||
value={getSecretURL()}
|
||||
onFocus={handleFocus}
|
||||
@ -131,7 +138,6 @@ const Home = () => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div class={style.buttonWrapper}>
|
||||
{!secretId && (
|
||||
<Button buttonType="create" onClick={onSubmit}>
|
||||
@ -157,6 +163,10 @@ const Home = () => {
|
||||
{error && <Error>{error}</Error>}
|
||||
|
||||
<Info>The secret link only works once, and then it will disappear.</Info>
|
||||
|
||||
<Info>
|
||||
<strong>Hemmelig</strong>, [he`m:(ə)li], means secret in Norwegian.
|
||||
</Info>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -4,15 +4,6 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.form {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.p {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
|
@ -21,6 +21,10 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--color-font);
|
||||
@ -33,47 +37,59 @@ a:hover {
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 7px;
|
||||
margin: var(--margin-basic);
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 22px;
|
||||
margin: var(--margin-basic) 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 7px 0;
|
||||
font-size: 14px;
|
||||
margin: var(--margin-basic) 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
padding: 20px 10px;
|
||||
#app-inner {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
max-width: 1024px;
|
||||
height: auto;
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
max-width: 850px;
|
||||
margin-bottom: var(--margin-basic);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 5px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-contrast: #2a9d8f;
|
||||
--color-font-contrast: #fcfcfc;
|
||||
--color-contrast-second: #577590;
|
||||
--color-background: #f9f9f9;
|
||||
--color-background: #fcfcfc;
|
||||
--color-font: #232323;
|
||||
--color-dark: #030303;
|
||||
--color-success: #43aa8b;
|
||||
--color-error: #f94144;
|
||||
--border-radius: 10px;
|
||||
--margin-basic: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
#app {
|
||||
padding: 56px 20px;
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,16 @@ const { hash, compare } = require('../helpers/password');
|
||||
const validUsername = new RegExp('^[A-Za-z0-9_-]*$');
|
||||
|
||||
const PASSWORD_LENGTH = 5;
|
||||
const USERNAME_LENGTH = 4;
|
||||
|
||||
async function authentication(fastify) {
|
||||
fastify.post('/signup', async (request, reply) => {
|
||||
const { username = '', password = '' } = request.body;
|
||||
|
||||
if (!validUsername.test(username)) {
|
||||
return reply.code(403).send({ error: 'Not a valid username' });
|
||||
if (!validUsername.test(username) || username.length < USERNAME_LENGTH) {
|
||||
return reply.code(403).send({
|
||||
error: `Has to be longer than ${USERNAME_LENGTH}, and can only contain these characters. [A-Za-z0-9_-]`,
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < PASSWORD_LENGTH) {
|
||||
|