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");
}