From bfe9c5b628f9288b5d4c943d392cc4e041d2957b Mon Sep 17 00:00:00 2001 From: Lukas SP Date: Sun, 23 Aug 2020 20:02:51 +0200 Subject: [PATCH] Implement frontend --- .gitignore | 3 +- internal/pastes/paste.go | 2 +- web/css/style.css | 172 +++++++++++++++++++++++++++++++++++++++ web/css/style.css.map | 9 ++ web/css/style.scss | 130 +++++++++++++++++++++++++++++ web/index.html | 61 +++++++++++++- web/js/autoload.js | 53 ++++++++++++ web/js/buttons.js | 82 +++++++++++++++++++ web/js/rest.js | 56 +++++++++++++ 9 files changed, 565 insertions(+), 3 deletions(-) create mode 100644 web/css/style.css create mode 100644 web/css/style.css.map create mode 100644 web/css/style.scss create mode 100644 web/js/autoload.js create mode 100644 web/js/buttons.js create mode 100644 web/js/rest.js diff --git a/.gitignore b/.gitignore index a04b42d..bd326a0 100644 --- a/.gitignore +++ b/.gitignore @@ -114,4 +114,5 @@ modules.xml # End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,go -web/*.gz \ No newline at end of file +web/*.gz +data/ \ No newline at end of file diff --git a/internal/pastes/paste.go b/internal/pastes/paste.go index 85570dd..1fb6ec0 100644 --- a/internal/pastes/paste.go +++ b/internal/pastes/paste.go @@ -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 } diff --git a/web/css/style.css b/web/css/style.css new file mode 100644 index 0000000..6658937 --- /dev/null +++ b/web/css/style.css @@ -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 */ \ No newline at end of file diff --git a/web/css/style.css.map b/web/css/style.css.map new file mode 100644 index 0000000..b00d674 --- /dev/null +++ b/web/css/style.css.map @@ -0,0 +1,9 @@ +{ + "version": 3, + "mappings": "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", + "sources": [ + "style.scss" + ], + "names": [], + "file": "style.css" +} \ No newline at end of file diff --git a/web/css/style.scss b/web/css/style.scss new file mode 100644 index 0000000..3d51219 --- /dev/null +++ b/web/css/style.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/web/index.html b/web/index.html index 735d3d1..3f9a720 100644 --- a/web/index.html +++ b/web/index.html @@ -4,8 +4,67 @@ pasty + + - frontend + + +
+
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/web/js/autoload.js b/web/js/autoload.js new file mode 100644 index 0000000..8d3ca23 --- /dev/null +++ b/web/js/autoload.js @@ -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", "
") + : hljs.highlightAuto(data.content).value.replace("\n", "
"); + for (i = 1; i <= data.content.split(/\n/).length; i++) { + document.getElementById("linenos").innerHTML += "" + i + ""; + } + + // 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); +} \ No newline at end of file diff --git a/web/js/buttons.js b/web/js/buttons.js new file mode 100644 index 0000000..bc9c1f2 --- /dev/null +++ b/web/js/buttons.js @@ -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); +} \ No newline at end of file diff --git a/web/js/rest.js b/web/js/rest.js new file mode 100644 index 0000000..f43ded4 --- /dev/null +++ b/web/js/rest.js @@ -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)); + }); +} \ No newline at end of file