diff --git a/.prettierrc b/.prettierrc
index c999600..94c37cc 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -3,5 +3,15 @@
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
- "tabWidth": 2
+ "tabWidth": 2,
+ "overrides": [
+ {
+ "files": "public/admin/**/*.js",
+ "options": {
+ "singleQuote": false,
+ "tabWidth": 4,
+ "printWidth": 100
+ }
+ }
+ ]
}
diff --git a/public/admin/handlers.js b/public/admin/handlers.js
new file mode 100644
index 0000000..30503e5
--- /dev/null
+++ b/public/admin/handlers.js
@@ -0,0 +1,524 @@
+var activeTab = null;
+
+// --------------------------------------------------------------- //
+
+function initView() {
+ // TODO: look at #tag & load the current page
+ // TODO: also, add support for this tag
+ home.display();
+}
+
+function updateActiveTab(newTab) {
+ if (activeTab != null && activeTab.ID == newTab.ID) {
+ return;
+ } else {
+ console.log("setting active tab to " + newTab.ID);
+ activeTab = newTab;
+
+ for (const child of document.getElementById("view-content").children) {
+ child.style.display = "none";
+ }
+ document.getElementById(activeTab.ID + "-content").style.removeProperty("display");
+ }
+}
+
+function createViewContent(tab_id) {
+ let viewContentChild = document.createElement("div");
+ viewContentChild.id = tab_id + "-content";
+ viewContentChild.style.display = "none";
+
+ document.getElementById("view-content").appendChild(viewContentChild);
+}
+
+function updateContents(tab_id, html) {
+ document.getElementById(tab_id + "-content").innerHTML = html;
+}
+function destroyContents(tab_id) {
+ document.getElementById(tab_id + "-content").innerHTML = "";
+}
+
+// tab is the tab button
+function createTab(tab) {
+ let tabList = document.getElementById("tab-list");
+
+ // only create the tab if it doesn't already exist
+ if (tabList.querySelector("#" + tab.ID + "-tab") != null) return;
+
+ let tabElement = document.createElement("div");
+ //
+
+ tabElement.id = tab.ID + "-tab";
+ tabElement.classList.add("tab-item");
+ tabElement.innerHTML = tab.NAME;
+ tabElement.addEventListener("click", (_) => {
+ updateActiveTab(tab);
+ });
+
+ let xImg = document.createElement("img");
+ xImg.src = "/static/icons/x.svg";
+ xImg.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ destroyContents(tab.ID);
+ tabElement.remove();
+ // TODO: does this know what home is?
+ updateActiveTab(home);
+ });
+
+ tabElement.append(xImg);
+
+ tabList.append(tabElement);
+}
+
+// --------------------------------------------------------------- //
+
+class Home {
+ constructor() {
+ this.ID = "home";
+ this.NAME = "⌂";
+ createViewContent(this.ID);
+ }
+
+ createContents() {
+ updateContents(this.ID, `
home
`);
+ }
+}
+
+class Officers {
+ constructor() {
+ this.ID = "officers";
+ this.NAME = "Officers";
+ createViewContent(this.ID);
+ }
+
+ createTab() {
+ // TODO: add support for this
+ /*
+ if (document.getElementById(this.ID + "-content").innerHTML == "") {
+ createTab(this);
+ this.createContents();
+ }
+ updateActiveTab(this);
+ */
+ }
+}
+
+// TODO: this is way overcomplicated; let's do this differently pls!
+// or at least clean up all this wildness......
+class AddNewOfficers {
+ constructor() {
+ this.ID = "add-new-officers";
+ this.NAME = "Add New Officers";
+ createViewContent(this.ID);
+ }
+
+ createTab() {
+ if (document.getElementById(this.ID + "-content").innerHTML == "") {
+ createTab(this);
+ this.createContents();
+ }
+ updateActiveTab(this);
+ }
+
+ createContents() {
+ let widget = `
+
+ `;
+ let contents = `
+
+
+
+ Add New Officer
+
+
+

+
+
+
+
+ ${widget}
+
+
+
+
+
+
+
+ `;
+ updateContents(this.ID, contents);
+
+ document.getElementById("add-new-officer-plus").addEventListener("click", (e) => {
+ let widgetElement = document.createElement("div");
+ widgetElement.innerHTML = widget;
+ document.getElementById("add-new-officer-widget-list").appendChild(widgetElement);
+ });
+ }
+
+ destroyContents() {
+ destroyContents(this.ID);
+ }
+
+ static update_checkbox(element) {
+ let confirmIconElement = element.parentElement.getElementsByClassName("confirm-icon")[0];
+ let closeElement =
+ element.parentElement.parentElement.parentElement.getElementsByClassName(
+ "close-icon"
+ )[0];
+
+ function makeDefault() {
+ confirmIconElement.src = "/static/icons/circle-question.svg";
+ confirmIconElement.setAttribute("data-checked", false);
+ confirmIconElement.style.filter = "invert(40%)";
+ element.style.borderColor = "#ccc";
+ element.style.backgroundColor = "#dbf4e000";
+ closeElement.style.filter = "";
+ closeElement.style.cursor = "pointer";
+ }
+
+ function makeGreen() {
+ confirmIconElement.src = "/static/icons/circle-check.svg";
+ confirmIconElement.setAttribute("data-checked", true);
+ confirmIconElement.style.filter =
+ "brightness(0) saturate(100%) invert(80%) sepia(19%) saturate(1059%) hue-rotate(92deg) brightness(90%) contrast(88%)";
+ element.style.borderColor = "#94dd94";
+ element.style.backgroundColor = "#dbf4e0";
+ closeElement.style.filter = "invert(80%)";
+ closeElement.style.cursor = "default";
+ }
+
+ function makeYellow() {
+ confirmIconElement.src = "/static/icons/circle-question.svg";
+ confirmIconElement.style.filter =
+ "brightness(0) saturate(100%) invert(80%) sepia(19%) saturate(2159%) hue-rotate(352deg) brightness(90%) contrast(88%)";
+ element.style.borderColor = "rgb(210 169 79)";
+ element.style.backgroundColor = "#f6d998";
+ }
+
+ let computingIdElement = element.parentElement.parentElement.getElementsByClassName(
+ "add-new-officer-computing-id"
+ )[0];
+ let positionElement = element.parentElement.parentElement.getElementsByClassName(
+ "add-new-officer-position"
+ )[0];
+ let startDateElement = element.parentElement.parentElement.getElementsByClassName(
+ "add-new-officer-start-date"
+ )[0];
+
+ // check that the data is well formed
+ if (confirmIconElement.getAttribute("data-checked") != "true") {
+ let name = computingIdElement.value;
+ let position = positionElement.value;
+ let startDate = startDateElement.value;
+
+ if (position == "" || name == "" || startDate == "") {
+ makeYellow();
+ return;
+ }
+ }
+
+ if (confirmIconElement.getAttribute("data-checked") == "true") {
+ makeDefault();
+
+ computingIdElement.readOnly = false;
+ // TODO: this setAttribute for input is a hacky workaround; fix it!
+ positionElement.removeAttribute("readonly");
+ startDateElement.readOnly = false;
+ } else {
+ makeGreen();
+
+ computingIdElement.readOnly = true;
+ positionElement.setAttribute("readonly", true);
+ startDateElement.readOnly = true;
+ }
+ }
+
+ static remove_widget(element) {
+ let confirmIconElement =
+ element.parentElement.parentElement.getElementsByClassName("confirm-icon")[0];
+ if (confirmIconElement.getAttribute("data-checked") != "true") {
+ element.parentElement.parentElement.remove();
+ }
+ }
+
+ static async onSubmit(element) {
+ let mainContainer = element.parentElement.parentElement;
+ let widgetList = mainContainer.getElementsByClassName(
+ "add-new-officer-input-widget-container"
+ );
+ let descText = document.getElementById("add-new-officer-desc-text");
+
+ let bodyObjectList = [];
+ for (let widget of widgetList) {
+ let status = widget.getElementsByClassName("add-new-officer-status")[0];
+ if (status.getAttribute("data-checked") != "true") {
+ descText.innerHTML = "must confirm all new officer info";
+ return;
+ }
+
+ let computingId = widget.getElementsByClassName("add-new-officer-computing-id")[0]
+ .value;
+ let position = widget.getElementsByClassName("add-new-officer-position")[0].value;
+ let startDate = widget.getElementsByClassName("add-new-officer-start-date")[0].value;
+ bodyObjectList.push({
+ computing_id: computingId,
+ position: position,
+ start_date: startDate
+ });
+ }
+
+ descText.innerHTML = "uploading ...";
+
+ let response = await fetch("/api/officers/new_term", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(bodyObjectList)
+ });
+
+ if (response.status == 500 || response.status == 422) {
+ descText.innerHTML = "failed to upload data, with http error " + response.status;
+ } else if (response.status != 200) {
+ descText.innerHTML =
+ "failed to upload data, with error (http " +
+ response.status +
+ "): " +
+ (await response.json()).detail;
+ } else {
+ descText.innerHTML = "success!";
+ }
+ }
+}
+
+class ViewOfficers {
+ constructor() {
+ this.ID = "view-officers";
+ this.NAME = "View Officers";
+ createViewContent(this.ID);
+ }
+
+ createTab() {
+ if (document.getElementById(this.ID + "-content").innerHTML == "") {
+ createTab(this);
+ this.createContents();
+ }
+ updateActiveTab(this);
+ }
+
+ createContents() {
+ let contents = `
+
+ `;
+
+ updateContents(this.ID, contents);
+ ViewOfficers.updateTable();
+ }
+
+ destroyContents() {
+ destroyContents(this.ID);
+ }
+
+ static async updateTable() {
+ let response = await fetch("/api/officers/all?view_only_filled_in=false", {
+ method: "GET"
+ });
+
+ let descText = document.getElementById("view-officers-desc-text");
+ if (response.status == 500 || response.status == 422) {
+ descText.innerHTML = "failed to get data, with error " + response.status;
+ } else if (response.status != 200) {
+ descText.innerHTML =
+ "failed to get, with error (http " +
+ response.status +
+ "): " +
+ (await response.json()).detail;
+ } else {
+ descText.innerHTML = "success!";
+ }
+
+ let positionInfoList = await response.json();
+ console.log(positionInfoList);
+
+ document.getElementById("view-officers-table").style.overflowX = "scroll";
+
+ let table = document.createElement("table");
+ table.id = "enterExecsTable";
+ table.style.height = "auto";
+ table.style.width = "200%";
+ table.style.fontSize = "14px";
+
+ let thead = document.createElement("thead");
+ let row = document.createElement("tr");
+ for (let key in positionInfoList[0]) {
+ let cell = document.createElement("th");
+
+ if (key == "private_data") {
+ continue;
+ } else if (key == "biography") {
+ cell.style.width = "30%";
+ }
+
+ cell.style.padding = "1ch";
+ cell.innerHTML = key.replaceAll("_", " ");
+ row.appendChild(cell);
+ }
+ // add private data
+ if (positionInfoList[0]["private_data"] != null) {
+ for (let key in positionInfoList[0]["private_data"]) {
+ let cell = document.createElement("th");
+ cell.style.padding = "1ch";
+ cell.innerHTML = key.replaceAll("_", " ");
+ row.appendChild(cell);
+ }
+ }
+ thead.appendChild(row);
+ table.appendChild(thead);
+
+ let tbody = document.createElement("tbody");
+ tbody.style.color = "white";
+
+ let runningIndex = 0;
+ for (let positionInfo of positionInfoList) {
+ let row = document.createElement("tr");
+ row.style.backgroundColor = runningIndex % 2 == 0 ? "#555" : "#111";
+ runningIndex += 1;
+
+ for (let key in positionInfo) {
+ if (key == "private_data") continue;
+ let cell = document.createElement("td");
+ cell.innerHTML = positionInfo[key];
+ cell.style.padding = "1ch";
+ cell.style.textAlign = "center";
+ row.appendChild(cell);
+ }
+ if (positionInfoList[0]["private_data"] != null) {
+ for (let key in positionInfo["private_data"]) {
+ let cell = document.createElement("td");
+ cell.innerHTML = positionInfo["private_data"][key];
+ cell.style.padding = "1ch";
+ cell.style.textAlign = "center";
+ row.appendChild(cell);
+ }
+ }
+ tbody.appendChild(row);
+ }
+
+ table.appendChild(tbody);
+ document.getElementById("view-officers-table").innerHTML = "";
+ document.getElementById("view-officers-table").appendChild(table);
+ }
+}
+
+class Device {
+ constructor() {
+ this.ID = "device";
+ this.NAME = "Device";
+ createViewContent(this.ID);
+ }
+
+ createTab() {
+ // TODO: add support for this
+ /*
+ if (document.getElementById(this.ID + "-content").innerHTML == "") {
+ createTab(this);
+ //this.createContents();
+ }
+ updateActiveTab(this);
+ */
+ }
+}
+
+class Admin {
+ constructor() {
+ this.ID = "admin";
+ this.NAME = "Admin";
+ createViewContent(this.ID);
+ }
+
+ createTab() {
+ // TODO: add support for this
+ /*
+ if (document.getElementById(this.ID + "-content").innerHTML == "") {
+ createTab(this);
+ //this.createContents();
+ }
+ updateActiveTab(this);
+ */
+ }
+}
+
+// --------------------------------------------------------------- //
+
+const home = new Home();
+
+const officers = new Officers();
+const addNewOfficers = new AddNewOfficers();
+const viewOfficers = new ViewOfficers();
+
+const device = new Device();
+const admin = new Admin();
+
+updateActiveTab(home);
+home.createContents();
diff --git a/public/admin/index.html b/public/admin/index.html
new file mode 100644
index 0000000..e04857c
--- /dev/null
+++ b/public/admin/index.html
@@ -0,0 +1,107 @@
+
+
+
+
+
+ CSSS Admin Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add new officers
+
+
+ View officers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Officers Log
+
+
+ Election Log
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/admin/officers.css b/public/admin/officers.css
new file mode 100644
index 0000000..ccd8370
--- /dev/null
+++ b/public/admin/officers.css
@@ -0,0 +1,168 @@
+#add-new-officers-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ width: calc(100% - 2rem);
+ padding: 0 1rem;
+}
+
+.add-new-officer-input-widget-container {
+ display: flex; flex-direction: row; align-items: center;
+}
+
+.add-new-officer-input-widget {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ gap: 3rem;
+
+ margin-top: 1rem;
+
+ width: calc(100% - 2rem - 3rem);
+ padding: 1rem;
+
+ background-color: #eee;
+ border-radius: 0.5rem;
+}
+
+.add-new-officer-input-widget label {
+ display: block;
+ margin-bottom: 0.25rem;
+}
+
+.add-new-officer-input-widget input[type=text],
+.add-new-officer-input-widget input[type=date],
+.add-new-officer-input-widget select {
+ padding: 0.25rem;
+
+ border: #eee solid 3px;
+ border-radius: 0.25rem;
+ outline: none;
+
+ font-family: monospace;
+
+ background-color: #fff;
+}
+
+.add-new-officer-input-widget input[type=text]:focus,
+.add-new-officer-input-widget input[type=date]:focus,
+.add-new-officer-input-widget select:focus {
+ border: #aaa solid 3px;
+}
+
+.add-new-officer-input-widget .confirm {
+ display: block;
+
+ height: 2rem;
+ width: 8rem;
+
+ margin-left: 0.5rem;
+
+ border-radius: 0.5rem;
+ border-width: 3px;
+ border-color: #ccc;
+
+ text-align: center;
+
+ cursor: pointer;
+
+ background-color: #eee;
+}
+
+.add-new-officer-input-widget-container .close-icon {
+ margin-top: 1rem;
+ padding-left: 1rem;
+ height: 2rem;
+
+ cursor: pointer;
+
+ filter: invert(50%);
+ transition: 0.2s filter ease-in-out;
+}
+.add-new-officer-input-widget-container .close-icon:hover {
+ filter: invert(10%);
+}
+.add-new-officer-input-widget-container .close-icon:active {
+ transition: 0.01s filter ease-in-out;
+ filter: invert(50%);
+}
+
+#add-new-officer-plus {
+ display: flex;
+ justify-content: center;
+
+ margin-left: auto;
+ width: 2rem;
+}
+#add-new-officer-plus img {
+ display: block;
+
+ width: 2rem;
+ height: 2rem;
+ margin-top: 0.5rem;
+
+ border-radius: 4rem;
+ border: none;
+
+ cursor: pointer;
+
+ text-align: center;
+
+ filter: invert(50%);
+ transition: 0.2s filter ease-in-out;
+}
+#add-new-officer-plus img:hover {
+ filter: invert(10%);
+}
+#add-new-officer-plus img:active {
+ transition: 0.01s filter ease-in-out;
+ filter: invert(50%);
+}
+
+.add-new-officer-submit {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+.add-new-officer-submit button {
+ display: block;
+
+ height: 2rem;
+ width: 8rem;
+ margin-top: 0.5rem;
+
+ border-radius: 0.5rem;
+ border-width: 3px;
+ border-color: #ccc;
+
+ cursor: pointer;
+
+ text-align: center;
+
+ background-color: #eee;
+}
+
+/* --------------------------------------------- */
+
+#view-officers-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ width: calc(100% - 2rem);
+ padding: 0 1rem;
+}
+
+.view-officers-submit {
+ display: flex;
+ justify-content: center;
+
+ margin-left: auto;
+}
+.view-officers-submit button {
+ cursor: pointer;
+ width: 8rem;
+ height: 2rem;
+}
+
+#view-officers-table {}
\ No newline at end of file
diff --git a/public/admin/script.js b/public/admin/script.js
new file mode 100644
index 0000000..a2496ba
--- /dev/null
+++ b/public/admin/script.js
@@ -0,0 +1,19 @@
+// TODO: implement hash routing using this
+
+/*
+function routeView() {
+ if (window.location.hash == '#execs') {
+ enterExecs();
+ } else if (window.location.hash == '#logs') {
+ enterLogs();
+ }
+}
+window.onload = routeView;
+window.onhashchange = routeView;
+
+
+function enterLogs() {
+ // TODO: flex center content
+ document.getElementById('content').innerHTML = 'nothing...';
+}
+s*/
diff --git a/public/admin/style.css b/public/admin/style.css
new file mode 100644
index 0000000..58ba471
--- /dev/null
+++ b/public/admin/style.css
@@ -0,0 +1,324 @@
+:root {
+ --body-width-max: 1480px;
+
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+
+ margin: 0 auto;
+
+ height: 100vh;
+
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+ font-weight: 400;
+ font-size: 1rem;
+
+ background-color: #fff;
+}
+
+a {
+ color: rgb(128, 189, 79);
+}
+
+a:hover {
+ color: rgb(36, 151, 117);
+}
+
+button {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+}
+
+select[readonly] option,
+select[readonly] optgroup {
+ display: none;
+}
+
+/* ------------------- organization -------------------- */
+
+#header {
+ width: 100%;
+
+ border-bottom: #bbb 1px solid;
+
+ background: rgb(184,252,223);
+ background: linear-gradient(19deg, rgb(202, 255, 232) 0%, rgb(236, 255, 218) 31%, rgba(255,254,243,1) 100%);
+}
+
+#header-content {
+ display: flex;
+ flex-direction: row;
+
+ height: 4.5rem;
+ margin: 0 auto;
+
+ max-width: var(--body-width-max);
+}
+
+#content {
+ display: flex;
+ flex-direction: row;
+
+ margin: 0 auto;
+ max-width: var(--body-width-max);
+ height: 64rem;
+
+ background-color: #fff;
+}
+
+#nav-bar {
+ display: flex;
+ flex-direction: column;
+
+ width: calc(16rem - 0.5rem);
+ height: calc(100% - 4rem - 1rem);
+
+ margin-top: 1rem;
+ padding-right: 0.5rem;
+
+ border-right: #ddd 1px solid;
+}
+
+#view-area {
+ width: calc(var(--body-width-max) - 16rem);
+ height: calc(100% - 4rem);
+}
+
+#tab-list {
+ display: flex;
+ flex-direction: row;
+
+ overflow: hidden;
+
+ height: 2rem;
+ padding-top: 1rem;
+ margin-left: 1rem;
+}
+
+#view-content {
+ height: calc(100% - 4rem);
+ margin: 0 1rem;
+
+ background-color: #fff;
+ border-top: #ddd 1px solid;
+}
+
+#footer {
+ display: flex;
+ flex-direction: row;
+
+ width: 100%;
+ height: 8rem;
+ border-top: #bbb 1px solid;
+
+ background-color: #fff;
+}
+
+/* ------------------- items -------------------- */
+
+#main-title {
+ display: flex;
+ align-items: center;
+ font-size: 1.5rem;
+ line-height: 1;
+
+ text-align: center;
+
+ padding: 0 1rem;
+ height: 100%;
+
+ vertical-align: middle;
+
+ cursor: pointer;
+ user-select: none;
+
+ font-family: Poppins, sans-serif;
+}
+
+#main-logo:hover {
+ filter: invert(15%);
+}
+
+.nav-header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ width: calc(100% - 2rem);
+ padding: 0.5rem 1rem;
+
+ border-radius: 0.5rem;
+ background-color: #fff;
+
+ cursor: pointer;
+ user-select: none;
+
+ font-family: Poppins, sans-serif;
+ font-weight: 700;
+}
+
+.nav-header:hover {
+ background-color: #eee;
+}
+.nav-header:active {
+ background-color: #f6f6f6;
+}
+
+.nav-children {
+ display: flex;
+ flex-direction: column;
+
+ width: 100%;
+
+ padding-left: 1rem;
+
+ color: #555;
+
+ font-family: Poppins, sans-serif;
+ font-weight: 400;
+}
+
+.nav-child {
+ width: calc(100% - 2rem);
+
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 1rem;
+
+ border-radius: 0.5rem;
+
+ cursor: pointer;
+ user-select: none;
+
+ transition: transform 0.15s ease-in-out;
+}
+.nav-child:hover {
+ background-color: #f7f7f7;
+ transform: translateX(-2px);
+}
+.nav-child:active {
+ transition: transform 0.06s ease-in-out;
+ transform: translateX(2px);
+}
+
+.tab-item {
+ height: 4rem;
+
+ border: #ddd 1px solid;
+ border-radius: 1.2rem;
+
+ cursor: pointer;
+ user-select: none;
+
+ padding: 0.3rem 1rem 0 1rem;
+ transition: transform 0.15s ease-in-out;
+}
+.tab-item:hover {
+ background-color: #eee;
+ border: #eee 1px solid;
+ transform: translateY(-2px);
+}
+.tab-item:active {
+ transition: transform 0.075s ease-in-out;
+
+ background-color: #f5f5f5;
+ border: #f5f5f5 1px solid;
+ transform: translateY(2px);
+}
+#home-tab {
+ font-size: 1.75rem;
+ height: 1.7rem;
+
+ cursor: pointer;
+ user-select: none;
+
+ padding: 0 1rem;
+ margin-top: -0.35rem;
+
+ transition: transform 0.15s ease-in-out;
+}
+#home-tab:hover {
+ transform: translateY(-1px);
+}
+#home-tab:active {
+ transition: transform 0.1s ease-in-out;
+ transform: translateY(2px);
+}
+
+.tab-item img {
+ height: 1rem;
+ margin-left: 0.5rem;
+ margin-bottom: -0.15rem;
+ border-radius: 0.25rem;
+
+ filter: invert(10%);
+
+ transition: 0.1s transform ease-in-out;
+}
+.tab-item img:hover {
+ filter: invert(0%);
+ background-color: #ccc;
+}
+.tab-item img:active {
+ filter: invert(20%);
+ background-color: #bbb;
+}
+
+/* ------------------- style attributes -------------------- */
+
+.active-tab {
+ background-color: #ddd;
+ border-color: #ddd;
+}
+.active-tab:hover {
+ background-color: #ccc;
+ border-color: #ccc;
+}
+.active-tab:active {
+ background-color: #d6d6d6;
+ border-color: #d6d6d6;
+}
+
+/* ------------------- old -------------------- */
+
+#controls {
+ display: flex;
+ gap: 2ch;
+
+ margin-top: -2ch;
+ padding: 1ch 0 0 1ch;
+}
+#controls div {
+ width: calc(50ch - 2ch);
+ padding: 1ch;
+}
+
+clickable {
+ color: rgb(250, 204, 119);
+ cursor: pointer;
+ text-decoration: underline;
+}
+clickable:hover {
+ color: rgb(235, 157, 94);
+}
+clickable:active {
+ color: rgb(255, 246, 162);
+}
+
+/*
+#content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+
+ width: 100ch;
+ height: 36ch;
+
+ margin: auto;
+
+ background-color: black;
+ color: grey;
+}
+*/
diff --git a/public/static/icons/circle-check.svg b/public/static/icons/circle-check.svg
new file mode 100644
index 0000000..72b9e7c
--- /dev/null
+++ b/public/static/icons/circle-check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/static/icons/circle-close.svg b/public/static/icons/circle-close.svg
new file mode 100644
index 0000000..0321dc2
--- /dev/null
+++ b/public/static/icons/circle-close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/static/icons/circle-plus.svg b/public/static/icons/circle-plus.svg
new file mode 100644
index 0000000..511bf8f
--- /dev/null
+++ b/public/static/icons/circle-plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/static/icons/circle-question.svg b/public/static/icons/circle-question.svg
new file mode 100644
index 0000000..98adbe3
--- /dev/null
+++ b/public/static/icons/circle-question.svg
@@ -0,0 +1,46 @@
+
+
diff --git a/public/static/icons/x.svg b/public/static/icons/x.svg
new file mode 100644
index 0000000..79f489b
--- /dev/null
+++ b/public/static/icons/x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file