fup: implement frontend bits

This commit is contained in:
Luke Granger-Brown 2021-03-21 23:10:15 +00:00
parent 45b3a1fe44
commit d05aa3ace1
5 changed files with 294 additions and 5 deletions

View file

@ -3,3 +3,135 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
html {
--colour-primary: #32681d;
--colour-primary-light: #619648;
--colour-primary-dark: #003d00;
--colour-text-primary: #ffffff;
--colour-text-primary-dark: #ffffff;
--colour-text-primary-light: #000000;
--colour-secondary: #cddc39;
--colour-secondary-light: #ffff6e;
--colour-secondary-dark: #99aa00;
--colour-text-secondary: #424242;
--colour-text-secondary-dark: #424242;
--colour-text-secondary-light: #424242;
--colour-canvas: #e1e2e1;
--colour-canvas-light: #f5f5f6;
--colour-text: #424242;
}
html {
color: var(--colour-text);
background: var(--colour-canvas);
font-family: "Roboto Sans", sans-serif;
}
body:not(.has-js) .with-js, body.has-js .no-js {
display: none;
}
body {
max-width: 800px;
margin: 0 auto;
}
header {
border-bottom: 1px solid var(--colour-primary-dark);
display: flex;
align-items: center;
}
header > h1 {
flex-grow: 1;
}
header > h1 > a, header > h1 > a:visited {
color: var(--colour-text);
text-decoration: none;
}
nav a:after {
display: inline;
content: "|";
padding-left: 0.3em;
opacity: 0.6;
}
nav a:last-of-type:after {
display: none;
}
a {
color: var(--colour-primary);
}
a:visited {
color: var(--colour-primary-dark);
}
header, .container {
padding: 10px 30px;
}
h1 {
font-family: "Roboto Mono", monospace;
}
.dropbox {
background: var(--colour-canvas-light);
height: 10rem;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.upload-list {
padding: 0;
}
.upload-list-element {
position: relative;
display: block;
border: 1px solid var(--colour-primary-dark);
margin-top: 0.4rem;
padding: 0 10px;
background: var(--colour-primary-light);
}
.upload-list-element, .upload-list-element a, .upload-list-element a:visited {
color: var(--colour-text-primary-light);
}
.upload-list-bar {
overflow: hidden;
position: absolute;
top: -1px;
left: 0;
background: var(--colour-primary-dark);
height: calc(100% + 2px);
width: 0;
}
.upload-list-bar-container {
padding: 1px 10px;
white-space: nowrap;
}
.upload-list-bar, .upload-list-bar a, .upload-list-bar a:visited {
color: var(--colour-text-primary-dark);
}
.uploaded {
background: var(--colour-primary-dark);
}
.uploaded a, .uploaded a:visited {
color: var(--colour-text-primary-dark);
}

View file

@ -1,3 +1,121 @@
// SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
//
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
document.body.classList.add('has-js');
if (document.body.classList.contains('upload-page')) {
const uploadListEl = document.body.querySelector('.upload-list');
const inputFileEl = document.body.querySelector('#file');
const expiryEl = document.body.querySelector('#expiry');
const uploadFile = (file, expiry) => {
const progressEl = document.createElement('li');
progressEl.classList.add('upload-list-element');
uploadListEl.appendChild(progressEl);
const bgEl = document.createElement('div');
bgEl.classList.add('upload-list-background');
progressEl.appendChild(bgEl);
const bgFilenameEl = document.createElement('span');
bgFilenameEl.classList.add('upload-list-filename');
bgFilenameEl.textContent = file.name;
bgEl.appendChild(bgFilenameEl);
const bgSpacerNode = document.createTextNode(' ');
bgEl.appendChild(bgSpacerNode);
const bgPercentEl = document.createElement('span');
bgPercentEl.classList.add('upload-list-progress');
bgPercentEl.textContent = file.size == 0 ? '(unknown)' : '(0%)';
bgEl.appendChild(bgPercentEl);
const barEl = document.createElement('div');
barEl.classList.add('upload-list-bar');
progressEl.appendChild(barEl);
const barContainerEl = document.createElement('div');
barContainerEl.classList.add('upload-list-bar-container');
barEl.appendChild(barContainerEl);
const barFilenameEl = document.createElement('span');
barFilenameEl.classList.add('upload-list-filename');
barFilenameEl.textContent = file.name;
barContainerEl.appendChild(barFilenameEl);
const barSpacerNode = document.createTextNode(' ');
barContainerEl.appendChild(barSpacerNode);
const barPercentEl = document.createElement('span');
barPercentEl.classList.add('upload-list-progress');
barPercentEl.textContent = file.size == 0 ? '(unknown)' : '(0%)';
barContainerEl.appendChild(barPercentEl);
const setPercentText = (txt) => {
barPercentEl.textContent = txt;
bgPercentEl.textContent = txt;
};
const setPercentPercent = (loaded, total) => {
const pct = Math.floor((loaded * 1000) / total) / 10;
setPercentText(`(${pct}%)`);
barEl.style.width = `${pct}%`;
};
const hdrs = new Headers();
hdrs.set('Content-Type', file.type);
hdrs.set('Accept', 'application/json');
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (ev) => {
if (!ev.lengthComputable) {
setPercentText(`(unknown: ${ev.loaded} bytes)`);
} else {
setPercentPercent(ev.loaded, ev.total);
}
});
xhr.upload.addEventListener('load', (ev) => {
setPercentPercent(1, 1);
});
xhr.addEventListener('error', (ev) => {
setPercentText(`(error: ${ev.message})`);
});
xhr.addEventListener('abort', (ev) => {
setPercentText(`(aborted)`);
});
xhr.addEventListener('load', (ev) => {
if (ev.status < 200 || ev.status >= 300) {
setPercentText(`(error: HTTP status ${ev.status} - ${ev.message})`);
return;
}
const respJSON = xhr.response;
while (progressEl.firstChild) {
progressEl.removeChild(progressEl.firstChild);
}
progressEl.classList.add('uploaded');
const linkEl = document.createElement('a');
linkEl.setAttribute('href', respJSON.display_url);
linkEl.textContent = file.name;
progressEl.appendChild(linkEl);
});
xhr.responseType = 'json';
xhr.open('PUT', `/upload/${encodeURIComponent(file.name)}`);
xhr.setRequestHeader('Content-Type', file.type);
xhr.setRequestHeader('Accept', 'application/json');
if (expiry !== "") {
xhr.setRequestHeader('Fup-Expiry', expiry);
}
xhr.send(file);
};
inputFileEl.addEventListener('change', (ev) => {
for (let i = 0; i < inputFileEl.files.length; i++) {
const file = inputFileEl.files[i];
uploadFile(file, expiryEl.value);
}
});
}

View file

@ -5,6 +5,6 @@ SPDX-License-Identifier: Apache-2.0
*/}}
{{define "main"}}
<h1>404 Not Found</h1>
<h2>404 Not Found</h2>
<p>Sorry. :(</p>
{{end}}

View file

@ -10,9 +10,20 @@ SPDX-License-Identifier: Apache-2.0
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="{{static "css/reset.min.css"}}">
<link rel="stylesheet" type="text/css" href="{{static "css/base.css"}}">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto&family=Roboto+Mono&display=swap" rel="stylesheet">
<script type="module" src="{{ static "js/base.js"}}"></script>
</head>
<body>
{{block "main" .}}{{end}}
<body class="{{block "extra_body_classes" .}}{{end}}">
<header>
<h1><a href="{{app "/"}}">lukegb</a></h1>
<nav>
<a href="/">Upload</a>
<a href="/paste">Paste</a>
</nav>
</header>
<div class="container">
{{block "main" .}}{{end}}
</div>
</body>
</html>

View file

@ -5,5 +5,33 @@ SPDX-License-Identifier: Apache-2.0
*/}}
{{define "main"}}
<h1>Hello</h1>
<label for="file">
<div class="dropbox with-js">
Click or drop to upload files
</div>
</label>
<form action="{{app "upload"}}" method="POST" enctype="multipart/form-data">
<div class="no-js">
<label for="file">Select file:</label> <input type="file" name="file" id="file" multiple> <input type="submit" value="Upload!">
</div>
<label for="expiry">Expiry:</label>
<select name="expiry" id="expiry">
<option value="" selected>never</option>
<option value="1m">1 minute</option>
<option value="5m">5 minutes</option>
<option value="1h">1 hour</option>
<option value="24h">1 day</option>
<option value="168h">1 week</option>
<option value="672h">4 weeks</option>
<option value="8760h">1 year</option>
</select>
</form>
<ol class="upload-list"></ol>
{{end}}
{{define "extra_body_classes" -}}
upload-page
{{- end}}