Add a SnackBar to show HTMx errors

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.
This commit is contained in:
jordi fita mas 2023-03-25 01:56:26 +01:00
parent 41ce5af2ed
commit 7e8ec539ff
3 changed files with 114 additions and 0 deletions

View File

@ -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 {

View File

@ -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();
},
}));
});

View File

@ -7,6 +7,7 @@
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
<script src="/static/htmx@1.8.6.min.js"></script>
<script type="module" src="/static/numerus.js"></script>
<script defer src="/static/alpinejs@3.12.0.min.js"></script>
</head>
<body>
<header>
@ -53,4 +54,18 @@
{{- template "content" . }}
</main>
</body>
<div x-data="snackbar">
<div x-show="show"
@click="dismiss"
x-cloak
x-transition:enter="enter"
x-transition:enter-start="start"
x-transition:enter-end="end"
x-transition:leave="leave"
x-transition:leave-start="start"
x-transition:leave-end="end"
role="alert">
<p x-text="toast"></p>
</div>
</div>
</html>