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 = ` +
+
+

+ View Officers +

+
+ +

+
+
+ +
+


+
+ `; + + 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 + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ ⌂ +
+
+
+ +
+
+
+ + + + + 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