* 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
This commit is contained in:
Bjarne Øverli 2021-06-21 14:41:21 +02:00 committed by GitHub
parent ad4971e4f7
commit 07bce75fd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 394 additions and 779 deletions

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 15 MiB

25
logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -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>
</>

View File

@ -1,5 +1,6 @@
.footer {
height: 25px;
height: auto;
padding: 10px;
font-size: 10px;
text-align: center;
}

View 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;

View File

@ -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);
}
}

View File

@ -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>
);

File diff suppressed because one or more lines are too long

View File

@ -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;
}
}

View 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;

View 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;

View 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;

View 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

View 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;
}

View 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;

View 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);
}
}

View File

@ -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);
}

View File

@ -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"
}

View File

@ -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>

View File

@ -1,5 +1,5 @@
.code {
margin: 7px 0;
margin: var(--margin-basic) 0;
width: 100%;
white-space: pre-wrap;
display: block;

View File

@ -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>
</>
);
};

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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) {