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" . }} +
+
+

+
+