From 7e8ec539fff7e256c164092e72faf365c00396d9 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Sat, 25 Mar 2023 01:56:26 +0100 Subject: [PATCH] Add a SnackBar to show HTMx errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We do not have any design yet for errors and other notifications, so i followed material design, for now, since we already kind of use their input fields design. This time i decided to use AlpineJS because there is not that much HTML code, and the transitioning is way easier to do in AlpineJS than it would be with plain JavaScript—not to mention the bugs i would introduce. --- web/static/numerus.css | 54 +++++++++++++++++++++++++++++++++++++++++ web/static/numerus.js | 45 ++++++++++++++++++++++++++++++++++ web/template/app.gohtml | 15 ++++++++++++ 3 files changed, 114 insertions(+) diff --git a/web/static/numerus.css b/web/static/numerus.css index a3895de..0854af1 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -702,6 +702,60 @@ tr.htmx-swapping td { transition: opacity 1s ease-out; } +/* Snackbar */ +[x-cloak] { + display: none !important; +} + +div[x-data="snackbar"] div[role="alert"] { + cursor: pointer; + background-color: var(--numerus--color--black); + color: var(--numerus--color--white); + padding: 2rem; + min-width: 28.8rem; + max-width: 56.8rem; + border-radius: 2px; + position: fixed; + translate: -50%; + left: 50%; + bottom: 0; +} + +div[x-data="snackbar"] div[role="alert"].enter, div[x-data="snackbar"] div[role="alert"].leave { + transition: transform; + transition-duration: 300ms; +} + +div[x-data="snackbar"] div[role="alert"].enter { + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); +} + +div[x-data="snackbar"] div[role="alert"].leave { + transition-timing-function: cubic-bezier(0, 0, 0.2, 1);; +} + +div[x-data="snackbar"] div[role="alert"].enter.start, div[x-data="snackbar"] div[role="alert"].leave.end { + transform: translateY(100%); +} + +div[x-data="snackbar"] div[role="alert"].enter p { + transition: opacity; + transition-delay: 150ms; + transition-duration: 300ms; +} + +div[x-data="snackbar"] div[role="alert"].enter.start p { + opacity: 0; +} + +div[x-data="snackbar"] div[role="alert"].enter.end p { + opacity: 1; +} + +div[x-data="snackbar"] div[role="alert"].enter.end, div[x-data="snackbar"] div[role="alert"].leave.start { + transform: translateY(0); +} + /* Remix Icon */ @font-face { diff --git a/web/static/numerus.js b/web/static/numerus.js index 345b202..4b778a3 100644 --- a/web/static/numerus.js +++ b/web/static/numerus.js @@ -426,3 +426,48 @@ htmx.on('closeModal', () => { openDialog.close(); openDialog.remove(); }); + +htmx.on(document, 'alpine:init', () => { + Alpine.data('snackbar', () => ({ + show: false, toast: "", toasts: [], timeoutId: null, init() { + htmx.on('htmx:error', (error) => { + this.showError(error.detail.errorInfo.error); + }); + }, + showError(message) { + this.toasts.push(message); + this.popUp(); + }, + popUp() { + if (this.toasts.length === 0) { + return; + } + if (this.show) { + this.dismiss(); + return; + } + if (this.toast !== "") { + // It will show after remove calls popUp again. + return; + } + this.toast = this.toasts[0]; + this.show = true; + this.timeoutId = setTimeout(this.dismiss.bind(this), 4000); + }, + dismiss() { + if (!this.show) { + // already dismissed + return; + } + this.show = false; + clearTimeout(this.timeoutId); + this.timeoutId = setTimeout(this.remove.bind(this), 350); + }, + remove() { + clearTimeout(this.timeoutId); + this.toasts.splice(0, 1); + this.toast = ""; + this.popUp(); + }, + })); +}); diff --git a/web/template/app.gohtml b/web/template/app.gohtml index 794818b..057d8b4 100644 --- a/web/template/app.gohtml +++ b/web/template/app.gohtml @@ -7,6 +7,7 @@ +
@@ -53,4 +54,18 @@ {{- template "content" . }} +
+
+

+
+