diff --git a/API.md b/API.md index fa3e62a..109ae07 100644 --- a/API.md +++ b/API.md @@ -173,9 +173,12 @@ POST /api/v2/pastes/{paste_id}/report **Response:** ```jsonc { + "success": true, // Whether or not the report was received successfully (this is returned by the report webhook to allow custom errors) "message": "message" // An optional message to display to the reporting user } ``` **Notes:** -* The endpoint is only available if the report system is enabled. Otherwise it will return a `404 Not Found` error. \ No newline at end of file +* The endpoint is only available if the report system is enabled. Otherwise it will return a `404 Not Found` error. +* The request for this endpoint is the exact same that will reach the webhook. +* The response from this endpoint is the exact same that pasty expects from the webhook. \ No newline at end of file diff --git a/internal/report/report.go b/internal/report/report.go index 6f11528..f147528 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -17,6 +17,7 @@ type ReportRequest struct { // ReportResponse represents a report response received from the report webhook type ReportResponse struct { + Success bool `json:"success"` Message string `json:"message"` } diff --git a/web/assets/css/style.css b/web/assets/css/style.css index e18d6bc..4ca65d9 100644 --- a/web/assets/css/style.css +++ b/web/assets/css/style.css @@ -7,6 +7,28 @@ html, body { font-family: 'Source Code Pro', monospace; } +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: none; +} + +::-webkit-scrollbar-thumb { + background: #444444; + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: #333333; +} + +::-webkit-scrollbar-thumb:active { + background: #222222; +} + .hidden { display: none; } @@ -33,7 +55,15 @@ html, body { } } -#spinner { +#spinner-container { + position: fixed; + top: 130px; + right: 20px; + height: 50px; + width: 50px; +} + +#spinner-container .spinner { -webkit-animation: .75s linear infinite spinner; animation: .75s linear infinite spinner; -webkit-animation-play-state: inherit; @@ -41,16 +71,32 @@ html, body { border: solid 5px #ffffff; border-bottom-color: transparent; border-radius: 50%; - height: 50px; - width: 50px; - position: fixed; - top: 130px; - right: 20px; + height: 100%; + width: 100%; -webkit-transform: translate3d(-50%, -50%, 0); transform: translate3d(-50%, -50%, 0); will-change: transform; } +#btn_report { + position: fixed; + bottom: 60px; + right: 30px; +} + +#btn_report svg { + -webkit-transition: all 250ms; + transition: all 250ms; +} + +#btn_report:hover { + cursor: pointer; +} + +#btn_report:hover svg { + stroke: #2daa57; +} + .navigation { position: fixed; top: 0; @@ -132,44 +178,57 @@ html, body { } .container #content { + -webkit-box-sizing: border-box; + box-sizing: border-box; padding: 20px; width: calc(100vw - 50px); } .container #content #code { - word-break: break-all; - white-space: pre-wrap; + white-space: pre; + line-height: 20px; + overflow-x: auto; } .container #content #input { height: 100%; width: 100%; + padding: 0; background-color: transparent; border: none; outline: none; color: inherit; resize: none; - font-size: 16px; + font: inherit; + line-height: 20px; } .container #notifications { position: fixed; - bottom: 0; + bottom: 30px; + right: 0; + padding: 20px; } .container #notifications div { - width: 100vw; - padding: 10px 20px; + border-radius: 10px; + width: 500px; + margin-top: 20px; + padding: 20px 30px; } .container #notifications div.error { - background-color: #ff3d3d; + background-color: #ff4d4d; } .container #notifications div.success { background-color: #389b38; } +.container #notifications div:first-child { + margin-top: 0; +} + #footer { position: fixed; bottom: 0; @@ -236,6 +295,15 @@ html, body { .navigation .meta #version { display: none; } + .container #notifications { + padding: 0; + } + .container #notifications div { + border-radius: 0; + width: 100vw; + -webkit-box-sizing: border-box; + box-sizing: border-box; + } #footer #flex { margin: 0 0 0 25px; } diff --git a/web/assets/css/style.css.map b/web/assets/css/style.css.map index 3d15a40..13102d1 100644 --- a/web/assets/css/style.css.map +++ b/web/assets/css/style.css.map @@ -1,6 +1,6 @@ { "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;CAyB5B;;AAlCD,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;;AAKb,AAAA,UAAU,CAAC;EACP,UAAU,EAAE,IAAI;EAChB,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;CAiDtB;;AApDD,AAII,UAJM,CAIJ,QAAQ,CAAC;EACP,OAAO,EAAE,MAAM;EACf,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,mBAAmB;EAC/B,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;CAUjB;;AAnBL,AAUQ,UAVE,CAIJ,QAAQ,CAMJ,IAAI,CAAC;EACH,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,MAAM;CAIrB;;AAlBT,AAeY,UAfF,CAIJ,QAAQ,CAMJ,IAAI,AAKD,WAAW,CAAC;EACT,aAAa,EAAE,IAAI;CACtB;;AAjBb,AAoBI,UApBM,CAoBJ,QAAQ,CAAC;EACP,OAAO,EAAE,IAAI;EACb,KAAK,EAAE,kBAAkB;CAe5B;;AArCL,AAuBQ,UAvBE,CAoBJ,QAAQ,CAGJ,KAAK,CAAC;EACJ,UAAU,EAAE,SAAS;EACrB,WAAW,EAAE,QAAQ;CACxB;;AA1BT,AA2BQ,UA3BE,CAoBJ,QAAQ,CAOJ,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;;AApCT,AAsCI,UAtCM,CAsCJ,cAAc,CAAC;EACb,QAAQ,EAAE,KAAK;EACf,MAAM,EAAE,CAAC;CAWZ;;AAnDL,AAyCQ,UAzCE,CAsCJ,cAAc,CAGV,GAAG,CAAC;EACF,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,SAAS;CAOrB;;AAlDT,AA4CY,UA5CF,CAsCJ,cAAc,CAGV,GAAG,AAGA,MAAM,CAAC;EACJ,gBAAgB,EAAE,OAAO;CAC5B;;AA9Cb,AA+CY,UA/CF,CAsCJ,cAAc,CAGV,GAAG,AAMA,QAAQ,CAAC;EACN,gBAAgB,EAAE,OAAO;CAC5B;;AAKb,AAAA,OAAO,CAAC;EACJ,QAAQ,EAAE,KAAK;EACf,MAAM,EAAE,CAAC;EACT,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI;EACX,gBAAgB,EAAE,OAAO;CA6B5B;;AAlCD,AAMI,OANG,CAMD,KAAK,CAAC;EACJ,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,WAAW,EAAE,MAAM;EACnB,eAAe,EAAE,aAAa;EAC9B,MAAM,EAAE,aAAa;CACxB;;AAZL,AAaI,OAbG,CAaD,GAAG,CAAC;EACF,OAAO,EAAE,YAAY;CACxB;;AAfL,AAgBI,OAhBG,CAgBD,CAAC,CAAC;EACA,OAAO,EAAE,YAAY;EACrB,eAAe,EAAE,IAAI;EACrB,KAAK,EAAE,OAAO;EACd,OAAO,EAAE,QAAQ;EACjB,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,SAAS;CAKxB;;AA3BL,AAuBQ,OAvBD,CAgBD,CAAC,AAOE,MAAM,CAAC;EACJ,gBAAgB,EAAE,OAAO;EACzB,KAAK,EAAE,OAAO;CACjB;;AA1BT,AA4BI,OA5BG,CA4BD,QAAQ,CAAC;EACP,OAAO,EAAE,YAAY;EACrB,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,QAAQ;EACjB,gBAAgB,EAAE,OAAO;CAC5B;;AAGL,MAAM,MAAM,MAAM,MAAM,SAAS,EAAE,KAAK;EACpC,AAAA,WAAW,CAAC;IACR,OAAO,EAAE,MAAM;IACf,KAAK,EAAE,kBAAkB;GAW5B;EAbD,AAGI,WAHO,CAGL,OAAO,CAAC;IACN,OAAO,EAAE,SAAS;GAKrB;EATL,AAKQ,WALG,CAGL,OAAO,CAEH,GAAG,CAAC;IACF,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;GACf;EART,AAUI,WAVO,CAUL,KAAK,CAAC,QAAQ,CAAC;IACb,OAAO,EAAE,IAAI;GAChB;EAIL,AACI,OADG,CACD,KAAK,CAAC;IACJ,MAAM,EAAE,UAAU;GACrB;EAHL,AAII,OAJG,CAID,kBAAkB,CAAC,IAAI,CAAC;IACtB,OAAO,EAAE,IAAI;GAChB;EANL,AAOI,OAPG,CAOD,CAAC,CAAC;IACA,OAAO,EAAE,QAAQ;GACpB;;;AAIT,MAAM,MAAM,MAAM,MAAM,SAAS,EAAE,KAAK;EACpC,AACI,OADG,CACD,KAAK,CAAC;IACJ,MAAM,EAAE,CAAC;IACT,eAAe,EAAE,YAAY;GAChC;EAJL,AAKI,OALG,CAKD,kBAAkB,CAAC;IACjB,OAAO,EAAE,IAAI;GAChB", + "mappings": "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", "sources": [ "style.scss" ], diff --git a/web/assets/css/style.scss b/web/assets/css/style.scss index 0717bb3..d1028e2 100644 --- a/web/assets/css/style.scss +++ b/web/assets/css/style.scss @@ -7,6 +7,29 @@ html, body { color: #ffffff; font-family: 'Source Code Pro', monospace; } + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: none; +} + +::-webkit-scrollbar-thumb { + background: #444444; + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: #333333; +} + +::-webkit-scrollbar-thumb:active { + background: #222222; +} + .hidden { display: none; } @@ -31,22 +54,41 @@ html, body { 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; +#spinner-container { position: fixed; top: 130px; right: 20px; - -webkit-transform: translate3d(-50%, -50%, 0); - transform: translate3d(-50%, -50%, 0); - will-change: transform; + height: 50px; + width: 50px; + & .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: 100%; + width: 100%; + -webkit-transform: translate3d(-50%, -50%, 0); + transform: translate3d(-50%, -50%, 0); + will-change: transform; + } +} + +#btn_report { + position: fixed; + bottom: 60px; + right: 30px; + & svg { + transition: all 250ms; + } + &:hover { + cursor: pointer; + & svg { + stroke: #2daa57; + } + } } .navigation { @@ -106,35 +148,46 @@ html, body { } } & #content { + box-sizing: border-box; padding: 20px; width: calc(100vw - 50px); & #code { - word-break: break-all; - white-space: pre-wrap; + white-space: pre; + line-height: 20px; + overflow-x: auto; } & #input { height: 100%; width: 100%; + padding: 0; background-color: transparent; border: none; outline: none; color: inherit; resize: none; - font-size: 16px; + font: inherit; + line-height: 20px; } } & #notifications { position: fixed; - bottom: 0; + bottom: 30px; + right: 0; + padding: 20px; & div { - width: 100vw; - padding: 10px 20px; + border-radius: 10px; + width: 500px; + margin-top: 20px; + padding: 20px 30px; &.error { - background-color: #ff3d3d; + background-color: #ff4d4d; } &.success { background-color: #389b38; } + &:first-child { + margin-top: 0; + } } } } @@ -191,6 +244,15 @@ html, body { } } + .container #notifications { + padding: 0; + & div { + border-radius: 0; + width: 100vw; + box-sizing: border-box; + } + } + #footer { & #flex { diff --git a/web/assets/js/api.js b/web/assets/js/api.js deleted file mode 100644 index 2d93ef8..0000000 --- a/web/assets/js/api.js +++ /dev/null @@ -1,38 +0,0 @@ -// apiBase defines the base URL of the API -const apiBase = location.protocol + "//" + location.host + "/api/v1"; - -// getAPIInformation returns the API information -export async function getAPIInformation() { - return fetch(apiBase + "/info"); -} - -// getPaste retrieves a paste -export async function getPaste(id) { - return fetch(apiBase + "/pastes/" + id); -} - -// createPaste creates a new paste -export async function createPaste(content) { - return await fetch(apiBase + "/pastes", { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - content - }) - }); -} - -// deletePaste deletes a paste -export async function deletePaste(id, deletionToken) { - return await fetch(apiBase + "/pastes/" + id, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - deletionToken - }) - }); -} \ No newline at end of file diff --git a/web/assets/js/app.js b/web/assets/js/app.js new file mode 100644 index 0000000..8e2080c --- /dev/null +++ b/web/assets/js/app.js @@ -0,0 +1,5 @@ +import * as Spinner from "./modules/spinner.js"; +import * as State from "./modules/state.js"; + +// Initialize the application state +Spinner.surround(State.initialize); diff --git a/web/assets/js/autoload.js b/web/assets/js/autoload.js deleted file mode 100644 index 2266cbb..0000000 --- a/web/assets/js/autoload.js +++ /dev/null @@ -1,113 +0,0 @@ -// Import the used modules -import * as api from "./api.js"; -import * as buttons from "./buttons.js"; -import * as spinner from "./spinner.js"; -import * as notifications from "./notifications.js"; - -// Set up the buttons -buttons.setupButtons(); -buttons.setupKeybinds(); - -// Define element handles -const versionElement = document.getElementById("version"); -const lineNOsElement = document.getElementById("linenos"); -const codeElement = document.getElementById("code"); -const inputElement = document.getElementById("input"); - -// Load the API information -async function loadAPIInformation() { - const response = await api.getAPIInformation(); - if (!response.ok) { - const data = await response.text(); - notifications.error("Failed fetching the API information: " + data + ""); - return; - } - const data = await response.json(); - versionElement.innerText = data.version; -} -loadAPIInformation(); - -// Try to load a paste if one exists -export let PASTE_ID; -let CODE; -async function loadPaste() { - if (location.pathname !== "/") { - // Define the paste ID and language - const split = location.pathname.replace("/", "").split("."); - const pasteID = split[0]; - const language = split[1]; - - // Retrieve the paste from the API and redirect the user to the main page if it could not be found - const response = await api.getPaste(pasteID); - if (!response.ok) { - location.replace(location.protocol + "//" + location.host); - return; - } - CODE = (await response.json()).content; - - // Adjust the button states - 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 - codeElement.innerHTML = language - ? hljs.highlight(language, CODE).value - : hljs.highlightAuto(CODE).value; - - // Display the line numbers - renderLineNumbers(); - window.addEventListener("resize", renderLineNumbers); - - // Set the PASTE_ID variable - PASTE_ID = pasteID; - } else { - inputElement.classList.remove("hidden"); - inputElement.focus(); - window.addEventListener("keydown", function (event) { - if (event.keyCode != 9) return; - event.preventDefault(); - - insertTextAtCursor(inputElement, " "); - }); - } -} -spinner.surround(loadPaste); - -function renderLineNumbers() { - lineNOsElement.innerHTML = CODE.split(/\n/).map((line, index) => { - let lineWidth = getTextWidth(line, "16px Source Code Pro"); - let linesSpace = Math.ceil(lineWidth / codeElement.offsetWidth); - - let result = `${index+1}`; - if (linesSpace > 1) { - result += "".repeat(linesSpace - 1); - } - return result; - }).join(""); -} - -// 1:1 skid from https://stackoverflow.com/questions/7404366/how-do-i-insert-some-text-where-the-cursor-is -function insertTextAtCursor(element, text) { - let value = element.value, endIndex, range, doc = element.ownerDocument; - if (typeof element.selectionStart == "number" - && typeof element.selectionEnd == "number") { - endIndex = element.selectionEnd; - element.value = value.slice(0, endIndex) + text + value.slice(endIndex); - element.selectionStart = element.selectionEnd = endIndex + text.length; - } else if (doc.selection != "undefined" && doc.selection.createRange) { - element.focus(); - range = doc.selection.createRange(); - range.collapse(false); - range.text = text; - range.select(); - } -} - -// Also a kind of skid -function getTextWidth(text, font) { - let canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); - let context = canvas.getContext("2d"); - context.font = font; - return context.measureText(text).width; -} \ No newline at end of file diff --git a/web/assets/js/buttons.js b/web/assets/js/buttons.js deleted file mode 100644 index b246c81..0000000 --- a/web/assets/js/buttons.js +++ /dev/null @@ -1,120 +0,0 @@ -// Import the used modules -import * as api from "./api.js"; -import * as autoload from "./autoload.js"; -import * as spinner from "./spinner.js"; -import * as notifications from "./notifications.js"; - -// setupKeybinds initializes the keybinds for the buttons -export function setupKeybinds() { - window.addEventListener("keydown", function (event) { - // Return if the CTRL key was not pressed - if (!event.ctrlKey) return; - - // Define the DOM element of the pressed button - 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 66: { - element = document.getElementById("btn_copy"); - break; - } - } - - // Call the onClick function of the button - if (element) { - if (element.hasAttribute("disabled")) return; - event.preventDefault(); - element.click(); - } - }); -} - -// setupButtons configures the click listeners of the buttons -export function setupButtons() { - // Define the behavior of the 'new' button - document.getElementById("btn_new").addEventListener("click", function () { - location.replace(location.protocol + "//" + location.host); - }); - - // Define the behavior of the 'save' button - document.getElementById("btn_save").addEventListener("click", function () { - spinner.surround(async function () { - // Return if the text area is empty - const input = document.getElementById("input"); - if (!input.value) return; - - // Create the paste - const response = await api.createPaste(input.value); - if (!response.ok) { - notifications.error("Failed creating the paste: " + await response.text() + ""); - return; - } - const data = await response.json(); - - // Give the user the chance to copy the deletion token - if (data.deletionToken) { - prompt("The deletion token for your paste is:", data.deletionToken); - } - - // Redirect the user to the paste page - let address = location.protocol + "//" + location.host + "/" + data.id; - if (data.suggestedSyntaxType) address += "." + data.suggestedSyntaxType; - location.replace(address); - }); - }); - - // Define the behavior of the 'delete' button - document.getElementById("btn_delete").addEventListener("click", function () { - spinner.surround(async function () { - // Ask the user for the deletion token - const deletionToken = prompt("Deletion Token:"); - if (!deletionToken) return; - - // Delete the paste - const response = await api.deletePaste(autoload.PASTE_ID, deletionToken); - const data = await response.text(); - if (!response.ok) { - notifications.error("Failed deleting the paste: " + data + ""); - return; - } - - // Redirect the user to the main page - location.replace(location.protocol + "//" + location.host); - }); - }); - - // Define the behavior of the 'copy' button - document.getElementById("btn_copy").addEventListener("click", function () { - spinner.surround(async function () { - // Ask for the clipboard permissions - askClipboardPermissions(); - - // Copy the code - await navigator.clipboard.writeText(document.getElementById("code").innerText); - notifications.success("Copied the code!"); - }); - }); -} - -// askClipboardPermissions asks the user for the clipboard permissions -async function askClipboardPermissions() { - try { - const state = await navigator.permissions.query({ - name: "clipboard-write" - }); - return state === "granted"; - } catch (error) { - return false; - } -} \ No newline at end of file diff --git a/web/assets/js/modules/animation.js b/web/assets/js/modules/animation.js new file mode 100644 index 0000000..174a846 --- /dev/null +++ b/web/assets/js/modules/animation.js @@ -0,0 +1,12 @@ +// Properly animates an element +export function animate(element, animation, duration, after) { + element.style.setProperty("--animate-duration", duration); + element.classList.add("animate__animated", animation); + element.addEventListener("animationend", () => { + element.style.removeProperty("--animate-duration"); + element.classList.remove("animate__animated", animation); + if (after) { + after(); + } + }, {once: true}); +} \ No newline at end of file diff --git a/web/assets/js/modules/api.js b/web/assets/js/modules/api.js new file mode 100644 index 0000000..6140672 --- /dev/null +++ b/web/assets/js/modules/api.js @@ -0,0 +1,57 @@ +const API_BASE_URL = location.protocol + "//" + location.host + "/api/v2"; + +export async function getAPIInformation() { + return fetch(API_BASE_URL + "/info"); +} + +export async function getPaste(pasteID) { + return fetch(API_BASE_URL + "/pastes/" + pasteID); +} + +export async function createPaste(content, metadata) { + return fetch(API_BASE_URL + "/pastes", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + content, + metadata + }) + }); +} + +export async function editPaste(pasteID, modificationToken, content, metadata) { + return fetch(API_BASE_URL + "/pastes/" + pasteID, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + modificationToken, + }, + body: JSON.stringify({ + content, + metadata + }) + }); +} + +export async function deletePaste(pasteID, modificationToken) { + return fetch(API_BASE_URL + "/pastes/" + pasteID, { + method: "DELETE", + headers: { + "Authorization": "Bearer " + modificationToken, + } + }); +} + +export async function reportPaste(pasteID, reason) { + return fetch(API_BASE_URL + "/pastes/" + pasteID + "/report", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + reason + }) + }); +} diff --git a/web/assets/js/modules/notifications.js b/web/assets/js/modules/notifications.js new file mode 100644 index 0000000..3a27b59 --- /dev/null +++ b/web/assets/js/modules/notifications.js @@ -0,0 +1,25 @@ +import * as Animation from "./animation.js"; + +const ELEMENT = document.getElementById("notifications"); + +// Shows a success notification +export function success(message) { + create("success", message, 3000); +} + +// Shows an error notification +export function error(message) { + create("error", message, 3000); +} + +// Creates a new custom notification +function create(type, message, duration) { + const node = document.createElement("div"); + node.classList.add(type); + Animation.animate(node, "animate__fadeInUp", "0.2s"); + node.innerHTML = message; + + ELEMENT.childNodes.forEach(child => Animation.animate(child, "animate__slideInUp", "0.2s")); + ELEMENT.appendChild(node); + setTimeout(() => Animation.animate(node, "animate__fadeOutUp", "0.2s", () => ELEMENT.removeChild(node)), duration); +} \ No newline at end of file diff --git a/web/assets/js/modules/spinner.js b/web/assets/js/modules/spinner.js new file mode 100644 index 0000000..201dc6b --- /dev/null +++ b/web/assets/js/modules/spinner.js @@ -0,0 +1,21 @@ +import * as Animation from "./animation.js"; + +const ELEMENT = document.getElementById("spinner-container"); + +// SHows the spinner +export function show() { + ELEMENT.classList.remove("hidden"); + Animation.animate(ELEMENT, "animate__zoomIn", "0.2s"); +} + +// Hides the spinner +export function hide() { + Animation.animate(ELEMENT, "animate__zoomOut", "0.2s", () => ELEMENT.classList.add("hidden")); +} + +// Surrounds an async action with a spinner +export async function surround(innerFunction) { + show(); + await innerFunction(); + hide(); +} \ No newline at end of file diff --git a/web/assets/js/modules/state.js b/web/assets/js/modules/state.js new file mode 100644 index 0000000..b3e2394 --- /dev/null +++ b/web/assets/js/modules/state.js @@ -0,0 +1,353 @@ +import * as API from "./api.js"; +import * as Notifications from "./notifications.js"; +import * as Spinner from "./spinner.js"; +import * as Animation from "./animation.js"; + +const CODE_ELEMENT = document.getElementById("code"); +const LINE_NUMBERS_ELEMENT = document.getElementById("linenos"); +const INPUT_ELEMENT = document.getElementById("input"); + +const BUTTONS_DEFAULT_ELEMENT = document.getElementById("buttons_default"); +const BUTTON_NEW_ELEMENT = document.getElementById("btn_new"); +const BUTTON_SAVE_ELEMENT = document.getElementById("btn_save"); +const BUTTON_EDIT_ELEMENT = document.getElementById("btn_edit"); +const BUTTON_DELETE_ELEMENT = document.getElementById("btn_delete"); +const BUTTON_COPY_ELEMENT = document.getElementById("btn_copy"); + +const BUTTON_REPORT_ELEMENT = document.getElementById("btn_report"); + +const BUTTONS_EDIT_ELEMENT = document.getElementById("buttons_edit"); +const BUTTON_EDIT_CANCEL_ELEMENT = document.getElementById("btn_edit_cancel"); +const BUTTON_EDIT_APPLY_ELEMENT = document.getElementById("btn_edit_apply"); + +let PASTE_ID; +let LANGUAGE; +let CODE; + +let EDIT_MODE = false; + +let API_INFORMATION = { + version: "error", + modificationTokens: false, + reports: false +}; + +// Initializes the state system +export async function initialize() { + loadAPIInformation(); + + setupButtonFunctionality(); + setupKeybinds(); + + if (location.pathname !== "/") { + // Extract the paste data (ID and language) + const split = location.pathname.replace("/", "").split("."); + const pasteID = split[0]; + const language = split[1]; + + // Try to retrieve the paste data from the API + const response = await API.getPaste(pasteID); + if (!response.ok) { + Notifications.error("Could not load paste: " + await response.text() + ""); + setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000); + return; + } + + // Set the persistent paste data + PASTE_ID = pasteID; + LANGUAGE = language; + CODE = (await response.json()).content; + + // Fill the code block with the just received data + updateCode(); + } else { + // Give the user the opportunity to paste his code + INPUT_ELEMENT.classList.remove("hidden"); + INPUT_ELEMENT.focus(); + } + + // Update the state of the buttons to match the current state + updateButtonState(); +} + +// Loads the API information +async function loadAPIInformation() { + // try to retrieve the API information + const response = await API.getAPIInformation(); + if (response.ok) { + API_INFORMATION = await response.json(); + } else { + Notifications.error("Failed loading API information: " + await response.text() + ""); + } + + // Display the API version + document.getElementById("version").innerText = API_INFORMATION.version; +} + +// Sets the current persistent code to the code block, highlights it and updates the line numbers +function updateCode() { + CODE_ELEMENT.innerHTML = LANGUAGE + ? hljs.highlight(LANGUAGE, CODE).value + : hljs.highlightAuto(CODE).value; + + LINE_NUMBERS_ELEMENT.innerHTML = CODE.split(/\n/).map((_, index) => `${index + 1}`).join(""); +} + +// Updates the button state according to the current state +function updateButtonState() { + if (PASTE_ID) { + BUTTON_SAVE_ELEMENT.setAttribute("disabled", true); + BUTTON_EDIT_ELEMENT.removeAttribute("disabled"); + BUTTON_DELETE_ELEMENT.removeAttribute("disabled"); + BUTTON_COPY_ELEMENT.removeAttribute("disabled"); + + if (API_INFORMATION.reports) { + BUTTON_REPORT_ELEMENT.classList.remove("hidden"); + } + } else { + BUTTON_SAVE_ELEMENT.removeAttribute("disabled"); + BUTTON_EDIT_ELEMENT.setAttribute("disabled", true); + BUTTON_DELETE_ELEMENT.setAttribute("disabled", true); + BUTTON_COPY_ELEMENT.setAttribute("disabled", true); + + if (API_INFORMATION.reports) { + BUTTON_REPORT_ELEMENT.classList.add("hidden"); + } + } +} + +// Toggles the edit mode +function toggleEditMode() { + if (EDIT_MODE) { + EDIT_MODE = false; + INPUT_ELEMENT.classList.add("hidden"); + CODE_ELEMENT.classList.remove("hidden"); + Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeOutDown", "0.3s", () => { + BUTTONS_EDIT_ELEMENT.classList.add("hidden"); + BUTTONS_DEFAULT_ELEMENT.classList.remove("hidden"); + Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeInDown", "0.3s"); + }); + } else { + EDIT_MODE = true; + CODE_ELEMENT.classList.add("hidden"); + INPUT_ELEMENT.classList.remove("hidden"); + INPUT_ELEMENT.value = CODE; + INPUT_ELEMENT.focus(); + Animation.animate(BUTTONS_DEFAULT_ELEMENT, "animate__fadeOutUp", "0.3s", () => { + BUTTONS_DEFAULT_ELEMENT.classList.add("hidden"); + BUTTONS_EDIT_ELEMENT.classList.remove("hidden"); + Animation.animate(BUTTONS_EDIT_ELEMENT, "animate__fadeInUp", "0.3s"); + }); + } +} + +// Sets up the keybinds for the buttons +function setupKeybinds() { + window.addEventListener("keydown", (event) => { + // All keybinds in the default button set include the CTRL key + if ((EDIT_MODE && !event.ctrlKey && event.code !== "Escape") || (!EDIT_MODE && !event.ctrlKey)) { + return; + } + + // Find the DOM element of the button to trigger + let element; + if (EDIT_MODE) { + switch (event.code) { + case "Escape": { + element = BUTTON_EDIT_CANCEL_ELEMENT; + break + } + case "KeyS": { + element = BUTTON_EDIT_APPLY_ELEMENT; + break; + } + } + } else { + switch (event.code) { + case "KeyQ": { + element = BUTTON_NEW_ELEMENT; + break; + } + case "KeyS": { + element = BUTTON_SAVE_ELEMENT; + break; + } + case "KeyO": { + element = BUTTON_EDIT_ELEMENT; + break; + } + case "KeyX": { + element = BUTTON_DELETE_ELEMENT; + break; + } + case "KeyB": { + element = BUTTON_COPY_ELEMENT; + break; + } + } + } + + // Trigger the found button + if (element) { + event.preventDefault(); + if (element.hasAttribute("disabled")) { + return; + } + element.click(); + } + }); + + // Additionally fix the behaviour of the Tab key + window.addEventListener("keydown", (event) => { + if (event.code != "Tab") { + return; + } + event.preventDefault(); + + insertTextAtCursor(inputElement, " "); + }); +} + +// Sets up the different button functionalities +function setupButtonFunctionality() { + BUTTON_NEW_ELEMENT.addEventListener("click", () => location.replace(location.protocol + "//" + location.host)); + + BUTTON_SAVE_ELEMENT.addEventListener("click", () => { + Spinner.surround(async () => { + // Only proceed if the input is not empty + if (!INPUT_ELEMENT.value) { + return; + } + + // Try to create the paste + const response = await API.createPaste(INPUT_ELEMENT.value); + if (!response.ok) { + Notifications.error("Error while creating paste: " + await response.text() + ""); + return; + } + const data = await response.json(); + + // Display the modification token if provided + if (data.modificationToken) { + prompt("The modification token for your paste is:", data.modificationToken); + } + + // Redirect the user to his newly created paste + location.replace(location.protocol + "//" + location.host + "/" + data.id); + }); + }); + + BUTTON_EDIT_ELEMENT.addEventListener("click", toggleEditMode); + + BUTTON_DELETE_ELEMENT.addEventListener("click", () => { + Spinner.surround(async () => { + // Ask for the modification token + const modificationToken = prompt("Modification token:"); + if (!modificationToken) { + return; + } + + // Try to delete the paste + const response = await API.deletePaste(PASTE_ID, modificationToken); + if (!response.ok) { + Notifications.error("Error while deleting paste: " + await response.text() + ""); + return; + } + + // Redirect the user to the start page + location.replace(location.protocol + "//" + location.host); + }); + }); + + BUTTON_COPY_ELEMENT.addEventListener("click", async () => { + // Ask for clipboard permissions + if (!(await askForClipboardPermission())) { + Notifications.error("Clipboard permission denied."); + return; + } + + // Copy the current code + await navigator.clipboard.writeText(CODE); + Notifications.success("Successfully copied the code."); + }); + + BUTTON_EDIT_CANCEL_ELEMENT.addEventListener("click", toggleEditMode); + + BUTTON_EDIT_APPLY_ELEMENT.addEventListener("click", async () => { + // Only proceed if the input is not empty + if (!INPUT_ELEMENT.value) { + return; + } + + // Ask for the modification token + const modificationToken = prompt("Modification token:"); + if (!modificationToken) { + return; + } + + // Try to edit the paste + const response = await API.editPaste(PASTE_ID, modificationToken, INPUT_ELEMENT.value); + if (!response.ok) { + Notifications.error("Error while editing paste: " + await response.text() + ""); + return; + } + + // Update the code and leave the edit mode + CODE = INPUT_ELEMENT.value; + updateCode(); + toggleEditMode(); + Notifications.success("Successfully edited paste."); + }); + + BUTTON_REPORT_ELEMENT.addEventListener("click", async () => { + // Ask the user for a reason + const reason = prompt("Reason:"); + if (!reason) { + return; + } + + // Try to report the paste + const response = await API.reportPaste(PASTE_ID, reason); + if (!response.ok) { + Notifications.error("Error while reporting paste: " + await response.text() + ""); + return; + } + + // Show the response message + const data = await response.json(); + if (!data.success) { + Notifications.error("Error while reporting paste: " + data.message + ""); + return; + } + Notifications.success(data.message); + }); +} + +// Asks for clipboard write permission +async function askForClipboardPermission() { + try { + const state = await navigator.permissions.query({ + name: "clipboard-write" + }); + return state.state === "granted" || state.state === "prompt"; + } catch (error) { + return false; + } +} + +// 1:1 skid from https://stackoverflow.com/questions/7404366/how-do-i-insert-some-text-where-the-cursor-is +function insertTextAtCursor(element, text) { + let value = element.value, endIndex, range, doc = element.ownerDocument; + if (typeof element.selectionStart == "number" + && typeof element.selectionEnd == "number") { + endIndex = element.selectionEnd; + element.value = value.slice(0, endIndex) + text + value.slice(endIndex); + element.selectionStart = element.selectionEnd = endIndex + text.length; + } else if (doc.selection != "undefined" && doc.selection.createRange) { + element.focus(); + range = doc.selection.createRange(); + range.collapse(false); + range.text = text; + range.select(); + } +} \ No newline at end of file diff --git a/web/assets/js/notifications.js b/web/assets/js/notifications.js deleted file mode 100644 index cdbd2df..0000000 --- a/web/assets/js/notifications.js +++ /dev/null @@ -1,22 +0,0 @@ -// element holds the notification containers DOM element -const element = document.getElementById("notifications"); - -// error shows an error notifications -export function error(message) { - create("error", message, 3000); -} - -// success shows a success notifications -export function success(message) { - create("success", message, 3000); -} - -// create creates a new notification -function create(type, message, duration) { - const node = document.createElement("div"); - node.classList.add(type); - node.innerHTML = message; - - element.appendChild(node); - setTimeout(() => element.removeChild(node), duration); -} \ No newline at end of file diff --git a/web/assets/js/spinner.js b/web/assets/js/spinner.js deleted file mode 100644 index f86801f..0000000 --- a/web/assets/js/spinner.js +++ /dev/null @@ -1,19 +0,0 @@ -// element holds the spinners DOM element -const element = document.getElementById("spinner"); - -// show shows the spinner -export function show() { - element.classList.remove("hidden"); -} - -// hide hides the spinner -export function hide() { - element.classList.add("hidden"); -} - -// surround surrounds an action with a spinner -export async function surround(action) { - show(); - await action(); - hide(); -} \ No newline at end of file diff --git a/web/index.html b/web/index.html index e2a7c06..f246eb7 100644 --- a/web/index.html +++ b/web/index.html @@ -7,12 +7,24 @@ pasty + - + + - + \ No newline at end of file