fup: implement frontend bits
This commit is contained in:
parent
45b3a1fe44
commit
d05aa3ace1
5 changed files with 294 additions and 5 deletions
|
@ -3,3 +3,135 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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);
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,121 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Luke Granger-Brown <depot@lukegb.com>
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,6 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
*/}}
|
*/}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<h1>404 Not Found</h1>
|
<h2>404 Not Found</h2>
|
||||||
<p>Sorry. :(</p>
|
<p>Sorry. :(</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -10,9 +10,20 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
<meta charset="utf-8">
|
<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/reset.min.css"}}">
|
||||||
<link rel="stylesheet" type="text/css" href="{{static "css/base.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>
|
<script type="module" src="{{ static "js/base.js"}}"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="{{block "extra_body_classes" .}}{{end}}">
|
||||||
{{block "main" .}}{{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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,5 +5,33 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
*/}}
|
*/}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{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}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "extra_body_classes" -}}
|
||||||
|
upload-page
|
||||||
|
{{- end}}
|
||||||
|
|
Loading…
Reference in a new issue