Implement frontend

This commit is contained in:
Lukas SP 2020-08-23 20:02:51 +02:00
parent d1720c92f2
commit bfe9c5b628
9 changed files with 565 additions and 3 deletions

3
.gitignore vendored
View File

@ -114,4 +114,5 @@ modules.xml
# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,go
web/*.gz
web/*.gz
data/

View File

@ -53,5 +53,5 @@ func (paste *Paste) HashDeletionToken() error {
// CheckDeletionToken checks whether or not the given deletion token is correct
func (paste *Paste) CheckDeletionToken(deletionToken string) bool {
match, err := argon2id.ComparePasswordAndHash(deletionToken, paste.DeletionToken)
return err != nil && match
return err == nil && match
}

172
web/css/style.css Normal file
View File

@ -0,0 +1,172 @@
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap");
html, body {
margin: 0;
padding: 0;
background-color: #000000;
color: #ffffff;
font-family: 'Source Code Pro', monospace;
}
.hidden {
display: none;
}
@-webkit-keyframes spinner {
0% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg);
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg);
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
@keyframes spinner {
0% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg);
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg);
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
#spinner {
-webkit-animation: .75s linear infinite spinner;
animation: .75s linear infinite spinner;
-webkit-animation-play-state: inherit;
animation-play-state: inherit;
border: solid 5px #ffffff;
border-bottom-color: transparent;
border-radius: 50%;
height: 50px;
width: 50px;
position: fixed;
top: 130px;
right: 20px;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
will-change: transform;
}
.navigation {
position: fixed;
top: 0;
width: calc(100vw - 80px);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
padding: 0 40px;
background-color: #222222;
}
.navigation .button {
padding: 10px 20px;
background-color: transparent;
border: none;
outline: none;
}
.navigation .button svg {
-webkit-transition: all 250ms;
transition: all 250ms;
}
.navigation .button:hover {
cursor: pointer;
}
.navigation .button:hover svg {
stroke: #2daa57;
}
.navigation .button:disabled svg {
stroke: #5a5a5a;
}
.navigation .button:disabled:hover {
cursor: initial;
color: initial;
}
.navigation .meta {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.navigation .meta #version {
margin-right: 40px;
padding: 5px 10px;
background-color: #000000;
border-radius: 10px;
}
.container {
margin-top: 60px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
}
.container #linenos {
padding: 20px 0;
width: 50px;
min-height: calc(100vh - 100px);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
background-color: #111111;
color: #bebebe;
}
.container #content {
padding: 20px;
width: calc(100vw - 50px);
}
.container #content #code {
white-space: pre;
}
.container #content #input {
height: 100%;
width: 100%;
background-color: transparent;
border: none;
outline: none;
color: inherit;
resize: none;
font-size: 16px;
}
/*# sourceMappingURL=style.css.map */

9
web/css/style.css.map Normal file
View File

@ -0,0 +1,9 @@
{
"version": 3,
"mappings": "AAAA,OAAO,CAAC,4EAAI;AAEZ,AAAA,IAAI,EAAE,IAAI,CAAC;EACP,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,4BAA4B;CAC5C;;AACD,AAAA,OAAO,CAAC;EACJ,OAAO,EAAE,IAAI;CAChB;;AAED,kBAAkB,CAAlB,OAAkB;EACd,EAAE;IACE,iBAAiB,EAAE,0BAA0B,CAAC,YAAY;IAClD,SAAS,EAAE,0BAA0B,CAAC,YAAY;;EAE9D,IAAI;IACA,iBAAiB,EAAE,0BAA0B,CAAC,cAAc;IACpD,SAAS,EAAE,0BAA0B,CAAC,cAAc;;;;AAGpE,UAAU,CAAV,OAAU;EACN,EAAE;IACE,iBAAiB,EAAE,0BAA0B,CAAC,YAAY;IAClD,SAAS,EAAE,0BAA0B,CAAC,YAAY;;EAE9D,IAAI;IACA,iBAAiB,EAAE,0BAA0B,CAAC,cAAc;IACpD,SAAS,EAAE,0BAA0B,CAAC,cAAc;;;;AAGpE,AAAA,QAAQ,CAAC;EACL,iBAAiB,EAAE,4BAA4B;EACvC,SAAS,EAAE,4BAA4B;EAC/C,4BAA4B,EAAE,OAAO;EAC7B,oBAAoB,EAAE,OAAO;EACrC,MAAM,EAAE,iBAAiB;EACzB,mBAAmB,EAAE,WAAW;EAChC,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,KAAK;EACV,KAAK,EAAE,IAAI;EACX,iBAAiB,EAAE,0BAA0B;EACrC,SAAS,EAAE,0BAA0B;EAC7C,WAAW,EAAE,SAAS;CACzB;;AAED,AAAA,WAAW,CAAC;EACR,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,kBAAkB;EACzB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,WAAW,EAAE,MAAM;EACnB,eAAe,EAAE,aAAa;EAC9B,OAAO,EAAE,MAAM;EACf,gBAAgB,EAAE,OAAO;CAoC5B;;AA7CD,AAUI,WAVO,CAUL,OAAO,CAAC;EACN,OAAO,EAAE,SAAS;EAClB,gBAAgB,EAAE,WAAW;EAC7B,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,IAAI;CAmBhB;;AAjCL,AAeQ,WAfG,CAUL,OAAO,CAKH,GAAG,CAAC;EACF,UAAU,EAAE,SAAS;CACxB;;AAjBT,AAkBQ,WAlBG,CAUL,OAAO,AAQJ,MAAM,CAAC;EACJ,MAAM,EAAE,OAAO;CAIlB;;AAvBT,AAoBY,WApBD,CAUL,OAAO,AAQJ,MAAM,CAED,GAAG,CAAC;EACF,MAAM,EAAE,OAAO;CAClB;;AAtBb,AAyBY,WAzBD,CAUL,OAAO,AAcJ,SAAS,CACJ,GAAG,CAAC;EACF,MAAM,EAAE,OAAO;CAClB;;AA3Bb,AA4BY,WA5BD,CAUL,OAAO,AAcJ,SAAS,AAIL,MAAM,CAAC;EACJ,MAAM,EAAE,OAAO;EACf,KAAK,EAAE,OAAO;CACjB;;AA/Bb,AAkCI,WAlCO,CAkCL,KAAK,CAAC;EACJ,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,WAAW,EAAE,MAAM;CAOtB;;AA5CL,AAsCQ,WAtCG,CAkCL,KAAK,CAID,QAAQ,CAAC;EACP,YAAY,EAAE,IAAI;EAClB,OAAO,EAAE,QAAQ;EACjB,gBAAgB,EAAE,OAAO;EACzB,aAAa,EAAE,IAAI;CACtB;;AAIT,AAAA,UAAU,CAAC;EACP,UAAU,EAAE,IAAI;EAChB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;CA4BtB;;AA/BD,AAII,UAJM,CAIJ,QAAQ,CAAC;EACP,OAAO,EAAE,MAAM;EACf,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,mBAAmB;EAC/B,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,MAAM;EACtB,WAAW,EAAE,MAAM;EACnB,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;CACjB;;AAbL,AAcI,UAdM,CAcJ,QAAQ,CAAC;EACP,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,kBAAkB;CAc5B;;AA9BL,AAiBQ,UAjBE,CAcJ,QAAQ,CAGJ,KAAK,CAAC;EACJ,WAAW,EAAE,GAAG;CACnB;;AAnBT,AAoBQ,UApBE,CAcJ,QAAQ,CAMJ,MAAM,CAAC;EACL,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,gBAAgB,EAAE,WAAW;EAC7B,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,OAAO;EACd,MAAM,EAAE,IAAI;EACZ,SAAS,EAAE,IAAI;CAClB",
"sources": [
"style.scss"
],
"names": [],
"file": "style.css"
}

130
web/css/style.scss Normal file
View File

@ -0,0 +1,130 @@
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap');
html, body {
margin: 0;
padding: 0;
background-color: #000000;
color: #ffffff;
font-family: 'Source Code Pro', monospace;
}
.hidden {
display: none;
}
@-webkit-keyframes spinner {
0% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg);
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg);
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
@keyframes spinner {
0% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(0deg);
transform: translate3d(-50%, -50%, 0) rotate(0deg);
}
100% {
-webkit-transform: translate3d(-50%, -50%, 0) rotate(360deg);
transform: translate3d(-50%, -50%, 0) rotate(360deg);
}
}
#spinner {
-webkit-animation: .75s linear infinite spinner;
animation: .75s linear infinite spinner;
-webkit-animation-play-state: inherit;
animation-play-state: inherit;
border: solid 5px #ffffff;
border-bottom-color: transparent;
border-radius: 50%;
height: 50px;
width: 50px;
position: fixed;
top: 130px;
right: 20px;
-webkit-transform: translate3d(-50%, -50%, 0);
transform: translate3d(-50%, -50%, 0);
will-change: transform;
}
.navigation {
position: fixed;
top: 0;
width: calc(100vw - 80px);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 40px;
background-color: #222222;
& .button {
padding: 10px 20px;
background-color: transparent;
border: none;
outline: none;
& svg {
transition: all 250ms;
}
&:hover {
cursor: pointer;
& svg {
stroke: #2daa57;
}
}
&:disabled {
& svg {
stroke: #5a5a5a;
}
&:hover {
cursor: initial;
color: initial;
}
}
}
& .meta {
display: flex;
flex-direction: row;
align-items: center;
& #version {
margin-right: 40px;
padding: 5px 10px;
background-color: #000000;
border-radius: 10px;;
}
}
}
.container {
margin-top: 60px;
display: flex;
flex-direction: row;
& #linenos {
padding: 20px 0;
width: 50px;
min-height: calc(100vh - 100px);
display: flex;
flex-direction: column;
align-items: center;
background-color: #111111;
color: #bebebe;
}
& #content {
padding: 20px;
width: calc(100vw - 50px);
& #code {
white-space: pre;
}
& #input {
height: 100%;
width: 100%;
background-color: transparent;
border: none;
outline: none;
color: inherit;
resize: none;
font-size: 16px;
}
}
}

View File

@ -4,8 +4,67 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pasty</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.2/build/styles/default.min.css">
</head>
<body>
frontend
<div id="spinner" class="hidden"></div>
<div class="navigation">
<div class="buttons">
<button class="button" id="btn_new" title="Create new paste (Ctrl + Q)">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-circle-plus" width="40" height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9" />
<line x1="9" y1="12" x2="15" y2="12" />
<line x1="12" y1="9" x2="12" y2="15" />
</svg>
</button>
<button class="button" id="btn_save" title="Save paste (Ctrl + S)">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-device-floppy" width="40" height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" />
<circle cx="12" cy="14" r="2" />
<polyline points="14 4 14 8 8 8 8 4" />
</svg>
</button>
<button class="button" id="btn_delete" title="Delete paste (Ctrl + X)" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="40" height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="4" y1="7" x2="20" y2="7" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</svg>
</button>
<button class="button" id="btn_copy" title="Copy paste to clipboard (Ctrl + C)" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard" width="40" height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M9 5H7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2V7a2 2 0 0 0 -2 -2h-2" />
<rect x="9" y="3" width="6" height="4" rx="2" />
</svg>
</button>
</div>
<div class="meta">
<div id="version">loading...</div>
<a class="button" title="View the GitHub repository" href="https://github.com/Lukaesebrot/pasty" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-github" width="40" height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M9 19c-4.286 1.35-4.286-2.55-6-3m12 5v-3.5c0-1 .099-1.405-.5-2 2.791-.3 5.5-1.366 5.5-6.04a4.567 4.567 0 0 0 -1.333 -3.21a4.192 4.192 0 0 0 -.08 -3.227s-1.05-.3-3.476 1.267a12.334 12.334 0 0 0 -6.222 0C6.462 2.723 5.413 3.023 5.413 3.023a4.192 4.192 0 0 0 -.08 3.227A4.566 4.566 0 0 0 4 9.486c0 4.64 2.709 5.68 5.5 6.014-.591.589-.56 1.183-.5 2V21" />
</svg>
</a>
</div>
</div>
<div class="container">
<div id="linenos"></div>
<div id="content">
<div id="code"></div>
<textarea id="input" class="hidden"></textarea>
</div>
</div>
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.2/build/highlight.min.js"></script>
<script src="js/rest.js"></script>
<script src="js/buttons.js"></script>
<script src="js/autoload.js"></script>
</body>
</html>

53
web/js/autoload.js Normal file
View File

@ -0,0 +1,53 @@
// Load the API information
loadAPIInfo();
// Set up the keybinds
setupKeybinds();
// Try to load a paste if one exists
let PASTE_ID = "";
function loadPaste() {
let split = location.pathname.split(".");
let pasteID = split[0];
let language = split[1];
getPaste(pasteID, function(success, data) {
// Return if no paste was found
if (!success) {
location.replace(location.protocol + "//" + location.host);
return;
};
// Enable and disable the corresponding buttons
document.getElementById("btn_save").setAttribute("disabled", true);
document.getElementById("btn_delete").removeAttribute("disabled");
document.getElementById("btn_copy").removeAttribute("disabled");
// Set the paste content to the DOM and display the line numbers
document.getElementById("code").innerHTML = language
? hljs.highlight(language, data.content).value.replace("\n", "<br />")
: hljs.highlightAuto(data.content).value.replace("\n", "<br />");
for (i = 1; i <= data.content.split(/\n/).length; i++) {
document.getElementById("linenos").innerHTML += "<span>" + i + "</span>";
}
// Set the PASTE_ID variable
PASTE_ID = pasteID;
});
}
if (location.pathname != "/") {
loadPaste();
} else {
const element = document.getElementById("input");
element.classList.remove("hidden");
element.focus();
}
// Define a function to copy text to the clipboard
function copyToClipboard(text) {
const element = document.createElement("textarea");
element.value = text;
document.body.appendChild(element);
element.select();
document.execCommand("copy");
document.body.removeChild(element);
}

82
web/js/buttons.js Normal file
View File

@ -0,0 +1,82 @@
// setupKeybinds initializes the keybinds for the buttons
function setupKeybinds() {
window.onkeydown = function(event) {
if (!event.ctrlKey) return;
let element = null;
switch (event.keyCode) {
case 81: {
element = document.getElementById("btn_new");
break;
}
case 83: {
element = document.getElementById("btn_save");
break;
}
case 88: {
element = document.getElementById("btn_delete");
break;
}
case 67: {
element = document.getElementById("btn_copy");
break;
}
}
if (element) {
if (element.hasAttribute("disabled")) return;
event.preventDefault();
element.onclick();
}
}
}
// Define the behavior of the 'new' button
document.getElementById("btn_new").onclick = function() {
location.replace(location.protocol + "//" + location.host);
}
// Define the behavior of the 'save' button
document.getElementById("btn_save").onclick = function() {
// Return if the text area is empty
if (!document.getElementById("input").value) return;
// Create the paste
createPaste(document.getElementById("input").value, function(success, data) {
// Notify the user about an error if one occurs
if (!success) {
alert("Error:\n\n" + data);
return;
}
// Redirect the user to the paste page
let address = location.protocol + "//" + location.host + "/" + data.id;
if (data.suggestedSyntaxType) address += "." + data.suggestedSyntaxType;
copyToClipboard(data.deletionToken);
location.replace(address);
});
}
// Define the behavior of the 'delete' button
document.getElementById("btn_delete").onclick = function() {
// Ask the user for the deletion token
let deletionToken = window.prompt("Deletion Token:");
if (!deletionToken) return;
// Delete the paste
deletePaste(PASTE_ID, deletionToken, function(success, data) {
// Notify the user about an error if one occurs
if (!success) {
alert("Error:\n\n" + data);
return;
}
// Redirect the user to the default page
location.replace(location.protocol + "//" + location.host);
});
}
// Define the behavior of the 'copy' button
document.getElementById("btn_copy").onclick = function() {
copyToClipboard(document.getElementById("code").innerText);
}

56
web/js/rest.js Normal file
View File

@ -0,0 +1,56 @@
// loadAPIInfo loads and displays the API information
function loadAPIInfo() {
fetch(location.protocol + "//" + location.host + "/api/v1/info")
.then(response => response.json())
.then(data => document.getElementById("version").innerText = data.version);
}
// getPaste retrieves a paste
function getPaste(id, callback) {
fetch(location.protocol + "//" + location.host + "/api/v1/pastes/" + id)
.then(response => {
if (response.status != 200) {
response.text().then(data => callback(false, data));
return;
}
response.json().then(data => callback(true, data));
});
}
// createPaste creates a new paste
function createPaste(content, callback) {
fetch(location.protocol + "//" + location.host + "/api/v1/pastes", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: content
})
}).then(response => {
if (response.status != 200) {
response.text().then(data => callback(false, data));
return;
}
response.json().then(data => callback(true, data));
});
}
// deletePaste deletes a paste
function deletePaste(id, deletionToken, callback) {
fetch(location.protocol + "//" + location.host + "/api/v1/pastes/" + id, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
deletionToken: deletionToken
})
}).then(response => {
if (response.status != 200) {
response.text().then(data => callback(false, data));
return;
}
response.text().then(data => callback(true, data));
});
}