diff --git a/relay/frontend/base.haml b/relay/frontend/base.haml index 992cfe7..873a8e2 100644 --- a/relay/frontend/base.haml +++ b/relay/frontend/base.haml @@ -13,7 +13,9 @@ %meta(name="viewport" content="width=device-width, initial-scale=1") %link(rel="stylesheet" type="text/css" href="/theme/{{config.theme}}.css" nonce="{{view.request['hash']}}") %link(rel="stylesheet" type="text/css" href="/static/style.css" nonce="{{view.request['hash']}}") + %link(rel="stylesheet" type="text/css" href="/static/toast.css" nonce="{{view.request['hash']}}") %script(type="application/javascript" src="/static/menu.js" nonce="{{view.request['hash']}}", defer) + %script(type="application/javascript" src="/static/toast.js" nonce="{{view.request['hash']}}", defer) %script(type="application/javascript" src="/static/api.js" nonce="{{view.request['hash']}}", defer) -block head @@ -37,6 +39,8 @@ -else {{menu_item("Login", "/login")}} + %ul#notifications + #container #header.section %span#menu-open << ⁞ diff --git a/relay/frontend/static/config.js b/relay/frontend/static/config.js index 5f592bf..edb63f5 100644 --- a/relay/frontend/static/config.js +++ b/relay/frontend/static/config.js @@ -18,7 +18,7 @@ async function handle_config_change(event) { await request("POST", "v1/config", params); } catch (error) { - alert(error); + toast(error); return; } @@ -26,6 +26,8 @@ async function handle_config_change(event) { document.querySelector("#header .title").innerHTML = params.value; document.querySelector("title").innerHTML = params.value; } + + toast("Updated config", "message"); } diff --git a/relay/frontend/static/domain_ban.js b/relay/frontend/static/domain_ban.js index e84bf52..4de2ebf 100644 --- a/relay/frontend/static/domain_ban.js +++ b/relay/frontend/static/domain_ban.js @@ -40,7 +40,7 @@ async function ban() { } if (values.domain === "") { - alert("Domain is required"); + toast("Domain is required"); return; } @@ -48,7 +48,7 @@ async function ban() { var ban = await request("POST", "v1/domain_ban", values); } catch (err) { - alert(err); + toast(err); return } @@ -58,8 +58,6 @@ async function ban() { remove: `` }); - console.log(row.querySelector(".update-ban")); - console.log(row.querySelector(".remove a")); add_row_listeners(row); elems.domain.value = null; @@ -67,6 +65,7 @@ async function ban() { elems.note.value = null; document.querySelector("details.section").open = false; + toast("Banned domain", "message"); } @@ -88,11 +87,12 @@ async function update_ban(domain) { await request("PATCH", "v1/domain_ban", values) } catch (error) { - alert(error); + toast(error); return; } row.querySelector("details").open = false; + toast("Updated baned domain", "message"); } @@ -101,11 +101,12 @@ async function unban(domain) { await request("DELETE", "v1/domain_ban", {"domain": domain}); } catch (error) { - alert(error); + toast(error); return; } document.getElementById(domain).remove(); + toast("Unbanned domain", "message"); } diff --git a/relay/frontend/static/instance.js b/relay/frontend/static/instance.js index 8987b25..8a4956f 100644 --- a/relay/frontend/static/instance.js +++ b/relay/frontend/static/instance.js @@ -35,7 +35,7 @@ async function add_instance() { } if (values.actor === "") { - alert("Actor is required"); + toast("Actor is required"); return; } @@ -43,7 +43,7 @@ async function add_instance() { var instance = await request("POST", "v1/instance", values); } catch (err) { - alert(err); + toast(err); return } @@ -62,6 +62,7 @@ async function add_instance() { elems.software.value = null; document.querySelector("details.section").open = false; + toast("Added instance", "message"); } @@ -70,7 +71,7 @@ async function del_instance(domain) { await request("DELETE", "v1/instance", {"domain": domain}); } catch (error) { - alert(error); + toast(error); return; } @@ -88,7 +89,7 @@ async function req_response(domain, accept) { await request("POST", "v1/request", params); } catch (error) { - alert(error); + toast(error); return; } @@ -115,6 +116,8 @@ async function req_response(domain, accept) { add_instance_listeners(row); } }); + + toast("Removed instance", "message"); } diff --git a/relay/frontend/static/software_ban.js b/relay/frontend/static/software_ban.js index 510f796..663929a 100644 --- a/relay/frontend/static/software_ban.js +++ b/relay/frontend/static/software_ban.js @@ -39,7 +39,7 @@ async function ban() { } if (values.name === "") { - alert("Domain is required"); + toast("Domain is required"); return; } @@ -47,7 +47,7 @@ async function ban() { var ban = await request("POST", "v1/software_ban", values); } catch (err) { - alert(err); + toast(err); return } @@ -64,6 +64,7 @@ async function ban() { elems.note.value = null; document.querySelector("details.section").open = false; + toast("Banned software", "message"); } @@ -85,11 +86,12 @@ async function update_ban(name) { await request("PATCH", "v1/software_ban", values) } catch (error) { - alert(error); + toast(error); return; } row.querySelector("details").open = false; + toast("Updated software ban", "message"); } @@ -98,11 +100,12 @@ async function unban(name) { await request("DELETE", "v1/software_ban", {"name": name}); } catch (error) { - alert(error); + toast(error); return; } document.getElementById(name).remove(); + toast("Unbanned software", "message"); } diff --git a/relay/frontend/static/toast.css b/relay/frontend/static/toast.css new file mode 100644 index 0000000..e544dcd --- /dev/null +++ b/relay/frontend/static/toast.css @@ -0,0 +1,68 @@ +#notifications { + position: fixed; + top: 40px; + left: 50%; + transform: translateX(-50%); +} + +#notifications li { + position: relative; + overflow: hidden; + list-style: none; + border-radius: 5px; + padding: 5px;; + margin-bottom: var(--spacing); + animation: show_toast 0.3s ease forwards; + display: grid; + grid-template-columns: auto max-content; + grid-gap: 5px; + align-items: center; +} + +#notifications a { + font-size: 1.5em; + line-height: 1em; + text-decoration: none; +} + +#notifications li.hide { + animation: hide_toast 0.3s ease forwards; +} + + +@keyframes show_toast { + 0% { + transform: translateX(100%); + } + + 40% { + transform: translateX(-5%); + } + + 80% { + transform: translateX(0%); + } + + 100% { + transform: translateX(-10px); + } +} + + +@keyframes hide_toast { + 0% { + transform: translateX(-10px); + } + + 40% { + transform: translateX(0%); + } + + 80% { + transform: translateX(-5%); + } + + 100% { + transform: translateX(calc(100% + 20px)); + } +} diff --git a/relay/frontend/static/toast.js b/relay/frontend/static/toast.js new file mode 100644 index 0000000..e4ca8cc --- /dev/null +++ b/relay/frontend/static/toast.js @@ -0,0 +1,26 @@ +const notifications = document.querySelector("#notifications") + + +function remove_toast(toast) { + toast.classList.add("hide"); + + if (toast.timeoutId) { + clearTimeout(toast.timeoutId); + } + + setTimeout(() => toast.remove(), 300); +} + +function toast(text, type="error", timeout=5) { + const toast = document.createElement("li"); + toast.className = `section ${type}` + toast.innerHTML = `${text}✖` + + toast.querySelector("a").addEventListener("click", async (event) => { + event.preventDefault(); + await remove_toast(toast); + }); + + notifications.appendChild(toast); + toast.timeoutId = setTimeout(() => remove_toast(toast), timeout * 1000); +} diff --git a/relay/frontend/static/user.js b/relay/frontend/static/user.js index 6f63334..9c74359 100644 --- a/relay/frontend/static/user.js +++ b/relay/frontend/static/user.js @@ -1,5 +1,4 @@ function add_row_listeners(row) { - console.log(row); row.querySelector(".remove a").addEventListener("click", async (event) => { event.preventDefault(); await del_user(row.id); @@ -23,12 +22,12 @@ async function add_user() { } if (values.username === "" | values.password === "" | values.password2 === "") { - alert("Username, password, and password2 are required"); + toast("Username, password, and password2 are required"); return; } if (values.password !== values.password2) { - alert("Passwords do not match"); + toast("Passwords do not match"); return; } @@ -36,7 +35,7 @@ async function add_user() { var user = await request("POST", "v1/user", values); } catch (err) { - alert(err); + toast(err); return } @@ -55,6 +54,7 @@ async function add_user() { elems.handle.value = null; document.querySelector("details.section").open = false; + toast("Created user", "message"); } @@ -63,11 +63,12 @@ async function del_user(username) { await request("DELETE", "v1/user", {"username": username}); } catch (error) { - alert(error); + toast(error); return; } document.getElementById(username).remove(); + toast("Deleted user", "message"); } diff --git a/relay/frontend/static/whitelist.js b/relay/frontend/static/whitelist.js index e04204d..70d4db1 100644 --- a/relay/frontend/static/whitelist.js +++ b/relay/frontend/static/whitelist.js @@ -11,7 +11,7 @@ async function add_whitelist() { var domain = domain_elem.value.trim(); if (domain === "") { - alert("Domain is required"); + toast("Domain is required"); return; } @@ -19,8 +19,8 @@ async function add_whitelist() { var item = await request("POST", "v1/whitelist", {"domain": domain}); } catch (err) { - alert(err); - return + toast(err); + return; } var row = append_table_row(document.getElementById("whitelist"), item.domain, { @@ -33,6 +33,7 @@ async function add_whitelist() { domain_elem.value = null; document.querySelector("details.section").open = false; + toast("Added domain", "message"); } @@ -41,11 +42,12 @@ async function del_whitelist(domain) { await request("DELETE", "v1/whitelist", {"domain": domain}); } catch (error) { - alert(error); + toast(error); return; } document.getElementById(domain).remove(); + toast("Removed domain", "message"); }