diff --git a/css/base.css b/css/base.css index 684be76..7c3b8cc 100644 --- a/css/base.css +++ b/css/base.css @@ -3,6 +3,7 @@ @import "modal.css"; @import "dark.css"; @import "tooltip.css"; +@import "loader.css"; :root { --border-width-default: 2px; @@ -233,7 +234,7 @@ sup.\~critical, .text-critical { padding-bottom: 0.1rem; margin-left: 0.5rem; margin-right: 1rem; - max-width: 50%; + max-width: 75%; } .stealth-input-hidden { diff --git a/css/loader.css b/css/loader.css new file mode 100644 index 0000000..2b7951a --- /dev/null +++ b/css/loader.css @@ -0,0 +1,40 @@ +.loader { + height: auto; + color: rgba(0, 0, 0, 0); +} + +.loader .dot { + --diameter: 0.5rem; + --radius: calc(var(--diameter) / 2); + --deviation: 20%; + height: var(--diameter); + width: var(--diameter); + background-color: var(--color-content); + border-radius: 50%; + position: absolute; + left: calc(50% - var(--radius)); + animation: osc 1s cubic-bezier(.72,.16,.31,.97) infinite; +} +.loader.loader-sm .dot { + --deviation: 10%; +} + +@keyframes osc { + 25% { + left: calc(50% + var(--deviation) - var(--radius)); + } + 75% { + left: calc(50% - var(--deviation)); + } + 0%, 50%, 100% { + left: calc(50% - var(--radius)); + } +/* + 0%, 100% { + left: calc(50% - var(--deviation)) + } + 50% { + left: calc(50% + var(--deviation) - var(--radius)); + } + */ +} diff --git a/html/admin.html b/html/admin.html index a1a3440..5bff46c 100644 --- a/html/admin.html +++ b/html/admin.html @@ -5,6 +5,10 @@ {{ template "header.html" . }} Admin - jfa-go @@ -15,15 +19,22 @@ Login - + - +
@@ -172,12 +192,14 @@ - -
- - +
+ +
+ + +
Create
@@ -202,20 +224,7 @@ Last Active - - - - Person Admin - - 13/12/20 00:39 - - - - Other person - - 12/12/20 17:46 - - + diff --git a/ts/admin.ts b/ts/admin.ts index 05f96a3..58e8887 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -2,7 +2,8 @@ import { toggleTheme, loadTheme } from "./modules/theme.js"; import { Modal } from "./modules/modal.js"; import { Tabs } from "./modules/tabs.js"; import { inviteList, createInvite } from "./modules/invites.js"; -import { _post, notificationBox, whichAnimationEvent } from "./modules/common.js"; +import { accountsList } from "./modules/accounts.js"; +import { _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js"; loadTheme(); (document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme; @@ -13,75 +14,37 @@ window.token = ""; window.availableProfiles = window.availableProfiles || []; +// load modals +(() => { + window.modals = {} as Modals; + + window.modals.login = new Modal(document.getElementById('modal-login'), true); + + window.modals.addUser = new Modal(document.getElementById('modal-add-user')); + + window.modals.about = new Modal(document.getElementById('modal-about')); + (document.getElementById('setting-about') as HTMLSpanElement).onclick = window.modals.about.toggle; + + window.modals.modifyUser = new Modal(document.getElementById('modal-modify-user')); + + window.modals.deleteUser = new Modal(document.getElementById('modal-delete-user')); + + window.modals.settingsRestart = new Modal(document.getElementById('modal-restart')); + + window.modals.settingsRefresh = new Modal(document.getElementById('modal-refresh')); + + window.modals.ombiDefaults = new Modal(document.getElementById('modal-ombi-defaults')); + document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiDefaults.close); +})(); + var inviteCreator = new createInvite(); +var accounts = new accountsList(); window.invites = new inviteList(); window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5); - -const loadAccounts = function () { - const rows: HTMLTableRowElement[] = Array.from(document.getElementById("accounts-list").children); - const selectAll = document.getElementById("accounts-select-all") as HTMLInputElement; - selectAll.onchange = () => { - for (let i = 0; i < rows.length; i++) { - (rows[i].querySelector("input[type=checkbox]") as HTMLInputElement).checked = selectAll.checked; - } - } - - const checkAllChecks = (state: boolean): boolean => { - let s = true; - for (let i = 0; i < rows.length; i++) { - const selectCheck = rows[i].querySelector("input[type=checkbox]") as HTMLInputElement; - if (selectCheck.checked != state) { - s = false; - break; - } - } - return s - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - const selectCheck = row.querySelector("input[type=checkbox]") as HTMLInputElement; - selectCheck.addEventListener("change", () => { - if (checkAllChecks(false)) { - selectAll.indeterminate = false; - selectAll.checked = false; - } else if (checkAllChecks(true)) { - selectAll.checked = true; - selectAll.indeterminate = false; - } else { - selectAll.indeterminate = true; - selectAll.checked = false; - } - - - }); - const editButton = row.querySelector(".icon") as HTMLElement; - const emailInput = row.querySelector(".input") as HTMLInputElement; - const outerClickListener = (event: Event) => { - if (!(event.target instanceof HTMLElement && (emailInput.contains(event.target) || editButton.contains(event.target)))) { - emailInput.classList.toggle('stealth-input-hidden'); - emailInput.readOnly = !emailInput.readOnly; - editButton.classList.toggle('ri-edit-line'); - editButton.classList.toggle('ri-check-line'); - document.removeEventListener('click', outerClickListener); - } - }; - editButton.onclick = function () { - emailInput.classList.toggle('stealth-input-hidden'); - emailInput.readOnly = !emailInput.readOnly; - editButton.classList.toggle('ri-edit-line'); - editButton.classList.toggle('ri-check-line'); - if (editButton.classList.contains('ri-check-line')) { - document.addEventListener('click', outerClickListener); - } - }; - } -}; - -const modifySettingsSource = function () { +/*const modifySettingsSource = function () { const profile = document.getElementById('radio-use-profile') as HTMLInputElement; const user = document.getElementById('radio-use-user') as HTMLInputElement; const profileSelect = document.getElementById('modify-user-profiles') as HTMLDivElement; @@ -92,64 +55,16 @@ const modifySettingsSource = function () { (profile.nextElementSibling as HTMLSpanElement).classList.toggle('!high'); profileSelect.classList.toggle('unfocused'); userSelect.classList.toggle('unfocused'); -} - -const radioUseProfile = document.getElementById('radio-use-profile') as HTMLInputElement; -radioUseProfile.addEventListener("change", modifySettingsSource); -radioUseProfile.checked = true; -const radioUseUser = document.getElementById('radio-use-user') as HTMLInputElement; -radioUseUser.addEventListener("change", modifySettingsSource); -radioUseUser.checked = false; - -const checkDeleteUserNotify = function () { - if ((document.getElementById('delete-user-notify') as HTMLInputElement).checked) { - document.getElementById('textarea-delete-user').classList.remove('unfocused'); - } else { - document.getElementById('textarea-delete-user').classList.add('unfocused'); - } -}; - -(document.getElementById('delete-user-notify') as HTMLInputElement).onchange = checkDeleteUserNotify; -checkDeleteUserNotify(); - +}*/ // load tabs window.tabs = new Tabs(); -for (let tabID of ["invitesTab", "accountsTab", "settingsTab"]) { +for (let tabID of ["invitesTab", "settingsTab"]) { window.tabs.addTab(tabID); } -window.tabs.addTab("accountsTab", loadAccounts); +window.tabs.addTab("accountsTab", null, accounts.reload); -// load modals -(() => { - window.modals = {} as Modals; - - window.modals.login = new Modal(document.getElementById('modal-login'), true); - - window.modals.addUser = new Modal(document.getElementById('modal-add-user')); - (document.getElementById('accounts-add-user') as HTMLSpanElement).onclick = window.modals.addUser.toggle; - document.getElementById('form-add-user').addEventListener('submit', window.modals.addUser.close); - - window.modals.about = new Modal(document.getElementById('modal-about')); - (document.getElementById('setting-about') as HTMLSpanElement).onclick = window.modals.about.toggle; - - window.modals.modifyUser = new Modal(document.getElementById('modal-modify-user')); - document.getElementById('form-modify-user').addEventListener('submit', window.modals.modifyUser.close); - (document.getElementById('accounts-modify-user') as HTMLSpanElement).onclick = window.modals.modifyUser.toggle; - - window.modals.deleteUser = new Modal(document.getElementById('modal-delete-user')); - document.getElementById('form-delete-user').addEventListener('submit', window.modals.deleteUser.close); - (document.getElementById('accounts-delete-user') as HTMLSpanElement).onclick = window.modals.deleteUser.toggle; - - window.modals.settingsRestart = new Modal(document.getElementById('modal-restart')); - - window.modals.settingsRefresh = new Modal(document.getElementById('modal-refresh')); - - window.modals.ombiDefaults = new Modal(document.getElementById('modal-ombi-defaults')); - document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiDefaults.close); -})(); - -function login(username: string, password: string) { +function login(username: string, password: string, run?: (state?: number) => void) { const req = new XMLHttpRequest(); req.responseType = 'json'; let url = window.URLBase; @@ -183,41 +98,28 @@ function login(username: string, password: string) { window.token = data["token"]; window.modals.login.close(); window.invites.reload(); - setInterval(window.invites.reload, 30*1000); + accounts.reload(); + setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000); document.getElementById("logout-button").classList.remove("unfocused"); - - /*generateInvites(); - setInterval((): void => generateInvites(), 60 * 1000); - addOptions(30, document.getElementById('days') as HTMLSelectElement); - addOptions(24, document.getElementById('hours') as HTMLSelectElement); - const minutes = document.getElementById('minutes') as HTMLSelectElement; - addOptions(59, minutes); - minutes.value = "30"; - checkDuration(); - if (modal) { - window.Modals.login.hide(); - } - Focus(document.getElementById('logoutButton')); - } - if (run) { - run(+this.status); - }*/ } + if (run) { run(+this.status); } } }; req.send(); } -document.getElementById('form-login').addEventListener('submit', (event: Event) => { +(document.getElementById('form-login') as HTMLFormElement).onsubmit = (event: SubmitEvent) => { event.preventDefault(); + const button = (event.target as HTMLElement).querySelector(".submit") as HTMLSpanElement; const username = (document.getElementById("login-user") as HTMLInputElement).value; const password = (document.getElementById("login-password") as HTMLInputElement).value; if (!username || !password) { window.notifications.customError("loginError", "The username and/or password were left blank."); return; } - login(username, password); -}); + toggleLoader(button); + login(username, password, () => toggleLoader(button)); +}; login("", ""); @@ -228,3 +130,4 @@ login("", ""); return false; } }); + diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts new file mode 100644 index 0000000..80c1593 --- /dev/null +++ b/ts/modules/accounts.ts @@ -0,0 +1,409 @@ +import { _get, _post, _delete, toggleLoader } from "../modules/common.js"; + +interface User { + id: string; + name: string; + email: string | undefined; + last_active: string; + admin: boolean; +} + +class user implements User { + private _row: HTMLTableRowElement; + private _check: HTMLInputElement; + private _username: HTMLSpanElement; + private _admin: HTMLSpanElement; + private _email: HTMLInputElement; + private _emailAddress: string; + private _emailEditButton: HTMLElement; + private _lastActive: HTMLTableDataCellElement; + id: string; + private _selected: boolean; + + get selected(): boolean { return this._selected; } + set selected(state: boolean) { + this._selected = state; + this._check.checked = state; + state ? document.dispatchEvent(this._checkEvent) : document.dispatchEvent(this._uncheckEvent); + } + + get name(): string { return this._username.textContent; } + set name(value: string) { this._username.textContent = value; } + + get admin(): boolean { return this._admin.classList.contains("chip"); } + set admin(state: boolean) { + if (state) { + this._admin.classList.add("chip", "~info", "ml-1"); + this._admin.textContent = "Admin"; + } else { + this._admin.classList.remove("chip", "~info", "ml-1"); + this._admin.textContent = "" + } + } + + get email(): string { return this._emailAddress; } + set email(value: string) { this._email.value = value; this._emailAddress = value; } + + get last_active(): string { return this._lastActive.textContent; } + set last_active(value: string) { this._lastActive.textContent = value; } + + private _checkEvent = new CustomEvent("accountCheckEvent"); + private _uncheckEvent = new CustomEvent("accountUncheckEvent"); + + constructor(user: User) { + this._row = document.createElement("tr") as HTMLTableRowElement; + this._row.innerHTML = ` + + + + + `; + this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement; + this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement; + this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement; + this._email = this._row.querySelector(".accounts-email") as HTMLInputElement; + this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement; + this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement; + this._check.onchange = () => { this.selected = this._check.checked; } + + const toggleStealthInput = () => { + this._email.classList.toggle("stealth-input-hidden"); + this._email.readOnly = !this._email.readOnly; + this._emailEditButton.classList.toggle("ri-check-line"); + this._emailEditButton.classList.toggle("ri-edit-line"); + }; + const outerClickListener = (event: Event) => { + if (!(event.target instanceof HTMLElement && (this._email.contains(event.target) || this._emailEditButton.contains(event.target)))) { + toggleStealthInput(); + this.email = this.email; + document.removeEventListener("click", outerClickListener); + } + }; + this._emailEditButton.onclick = () => { + if (this._email.classList.contains("stealth-input-hidden")) { + document.addEventListener('click', outerClickListener); + } else { + this._updateEmail(); + document.removeEventListener('click', outerClickListener); + } + toggleStealthInput(); + }; + + this.update(user); + } + + private _updateEmail = () => { + let oldEmail = this.email; + this.email = this._email.value; + let send = {}; + send[this.id] = this.email; + _post("/users/emails", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + if (req.status == 200) { + window.notifications.customPositive("emailChanged", "Success:", `Changed email address of "${this.name}".`); + } else { + this.email = oldEmail; + window.notifications.customError("emailChanged", `Couldn't change email address of "${this.name}".`); + } + } + }); + } + + update = (user: User) => { + this.id = user.id; + this.name = user.name; + this.email = user.email || ""; + this.last_active = user.last_active; + this.admin = user.admin; + } + + asElement = (): HTMLTableRowElement => { return this._row; } + remove = () => { + if (this.selected) { + document.dispatchEvent(this._uncheckEvent); + } + this._row.remove(); + } +} + + + + +export class accountsList { + private _table = document.getElementById("accounts-list") as HTMLTableSectionElement; + + private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement; + private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement; + private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement; + private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement; + private _modifySettings = document.getElementById("accounts-modify-user") as HTMLSpanElement; + private _modifySettingsProfile = document.getElementById("radio-use-profile") as HTMLInputElement; + private _modifySettingsUser = document.getElementById("radio-use-user") as HTMLInputElement; + private _profileSelect = document.getElementById("modify-user-profiles") as HTMLSelectElement; + private _userSelect = document.getElementById("modify-user-users") as HTMLSelectElement; + + private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement; + private _users: { [id: string]: user }; + private _checkCount: number = 0; + + private _addUserForm = document.getElementById("form-add-user") as HTMLFormElement; + private _addUserName = this._addUserForm.querySelector("input[type=text]") as HTMLInputElement; + private _addUserEmail = this._addUserForm.querySelector("input[type=email]") as HTMLInputElement; + private _addUserPassword = this._addUserForm.querySelector("input[type=password]") as HTMLInputElement; + + get selectAll(): boolean { return this._selectAll.checked; } + set selectAll(state: boolean) { + for (let id in this._users) { + this._users[id].selected = state; + } + this._selectAll.checked = state; + this._selectAll.indeterminate = false; + state ? this._checkCount = Object.keys(this._users).length : 0; + + } + + add = (u: User) => { + let domAccount = new user(u); + this._users[u.id] = domAccount; + this._table.appendChild(domAccount.asElement()); + } + + private _checkCheckCount = () => { + if (this._checkCount == 0) { + this._selectAll.indeterminate = false; + this._selectAll.checked = false; + this._modifySettings.classList.add("unfocused"); + this._deleteUser.classList.add("unfocused"); + } else { + if (this._checkCount == Object.keys(this._users).length) { + this._selectAll.checked = true; + this._selectAll.indeterminate = false; + } else { + this._selectAll.checked = false; + this._selectAll.indeterminate = true; + } + this._modifySettings.classList.remove("unfocused"); + this._deleteUser.classList.remove("unfocused"); + (this._checkCount == 1) ? this._deleteUser.textContent = "Delete User" : this._deleteUser.textContent = "Delete Users"; + } + } + + private _genCountString = (): string => { return `${this._checkCount} user${(this._checkCount > 1) ? "s" : ""}`; } + + private _collectUsers = (): string[] => { + let list: string[] = []; + for (let id in this._users) { + if (this._users[id].selected) { list.push(id); } + } + return list; + } + + private _addUser = (event: Event) => { + event.preventDefault(); + const button = this._addUserForm.querySelector("span.submit") as HTMLSpanElement; + const send = { + "username": this._addUserName.value, + "email": this._addUserEmail.value, + "password": this._addUserPassword.value + }; + for (let field in send) { + if (!send[field]) { + window.notifications.customError("addUserBlankField", "Fields were left blank."); + return; + } + } + toggleLoader(button); + _post("/users", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + if (req.status == 200) { + window.notifications.customPositive("addUser", "Success:", `user "${send['username']}" created.`); + } + this.reload(); + window.modals.addUser.close(); + } + }); + } + + deleteUsers = () => { + const modalHeader = document.getElementById("header-delete-user"); + modalHeader.textContent = this._genCountString(); + let list = this._collectUsers(); + const form = document.getElementById("form-delete-user") as HTMLFormElement; + const button = form.querySelector("span.submit") as HTMLSpanElement; + this._deleteNotify.checked = false; + this._deleteReason.value = ""; + this._deleteReason.classList.add("unfocused"); + form.onsubmit = (event: Event) => { + event.preventDefault(); + toggleLoader(button); + let send = { + "users": list, + "notify": this._deleteNotify.checked, + "reason": this._deleteNotify ? this._deleteReason.value : "" + }; + _delete("/users", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + window.modals.deleteUser.close(); + if (req.status != 200 && req.status != 204) { + let errorMsg = "Failed (check console/logs)."; + if (!("error" in req.response)) { + errorMsg = "Partial failure (check console/logs)."; + } + window.notifications.customError("deleteUserError", errorMsg); + } else { + window.notifications.customPositive("deleteUserSuccess", "Success:", `deleted ${this._genCountString()}.`); + } + this.reload(); + } + }); + }; + window.modals.deleteUser.show(); + } + + modifyUsers = () => { + const modalHeader = document.getElementById("header-modify-user"); + modalHeader.textContent = this._genCountString(); + let list = this._collectUsers(); + (() => { + let innerHTML = ""; + for (const profile of window.availableProfiles) { + innerHTML += ``; + } + this._profileSelect.innerHTML = innerHTML; + })(); + + (() => { + let innerHTML = ""; + for (let id in this._users) { + innerHTML += ``; + } + this._userSelect.innerHTML = innerHTML; + })(); + + const form = document.getElementById("form-modify-user") as HTMLFormElement; + const button = form.querySelector("span.submit") as HTMLSpanElement; + this._modifySettingsProfile.checked = true; + this._modifySettingsUser.checked = false; + form.onsubmit = (event: Event) => { + event.preventDefault(); + toggleLoader(button); + let send = { + "apply_to": list, + "homescreen": (document.getElementById("modify-user-homescreen") as HTMLInputElement).checked + }; + if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) { + send["from"] = "profile"; + send["profile"] = this._profileSelect.value; + } else if (this._modifySettingsUser.checked && !this._modifySettingsProfile.checked) { + send["from"] = "user"; + send["id"] = this._userSelect.value; + } + _post("/users/settings", send, (req: XMLHttpRequest) => { + if (req.readyState == 4) { + toggleLoader(button); + if (req.status == 500) { + let response = JSON.parse(req.response); + let errorMsg = ""; + if ("homescreen" in response && "policy" in response) { + const homescreen = Object.keys(response["homescreen"]).length; + const policy = Object.keys(response["policy"]).length; + if (homescreen != 0 && policy == 0) { + errorMsg = "Settings were applied, but applying homescreen layout may have failed."; + } else if (policy != 0 && homescreen == 0) { + errorMsg = "Homescreen layout was applied, but applying settings may have failed."; + } else if (policy != 0 && homescreen != 0) { + errorMsg = "Application failed."; + } + } else if ("error" in response) { + errorMsg = response["error"]; + } + window.notifications.customError("modifySettingsError", errorMsg); + } else if (req.status == 200 || req.status == 204) { + window.notifications.customPositive("modifySettingsSuccess", "Success:", `applied settings to ${this._genCountString()}.`); + } + this.reload(); + window.modals.modifyUser.close(); + } + }); + }; + window.modals.modifyUser.show(); + } + + + + constructor() { + this._users = {}; + this._selectAll.checked = false; + this._selectAll.onchange = () => { this.selectAll = this._selectAll.checked }; + document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); }); + document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); }); + this._addUserButton.onclick = window.modals.addUser.toggle; + this._addUserForm.addEventListener("submit", this._addUser); + + this._deleteNotify.onchange = () => { + if (this._deleteNotify.checked) { + this._deleteReason.classList.remove("unfocused"); + } else { + this._deleteReason.classList.add("unfocused"); + } + }; + this._modifySettings.onclick = this.modifyUsers; + this._modifySettings.classList.add("unfocused"); + const checkSource = () => { + const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement; + const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement; + if (this._modifySettingsProfile.checked) { + this._userSelect.parentElement.classList.add("unfocused"); + this._profileSelect.parentElement.classList.remove("unfocused") + profileSpan.classList.add("!high"); + profileSpan.classList.remove("!normal"); + userSpan.classList.remove("!high"); + userSpan.classList.add("!normal"); + } else { + this._userSelect.parentElement.classList.remove("unfocused"); + this._profileSelect.parentElement.classList.add("unfocused"); + userSpan.classList.add("!high"); + userSpan.classList.remove("!normal"); + profileSpan.classList.remove("!high"); + profileSpan.classList.add("!normal"); + } + }; + this._modifySettingsProfile.onchange = checkSource; + this._modifySettingsUser.onchange = checkSource; + + this._deleteUser.onclick = this.deleteUsers; + this._deleteUser.classList.add("unfocused"); + + if (!window.usernameEnabled) { + this._addUserName.classList.add("unfocused"); + this._addUserName = this._addUserEmail; + } + /*if (!window.emailEnabled) { + this._deleteNotify.parentElement.classList.add("unfocused"); + this._deleteNotify.checked = false; + }*/ + } + + reload = () => _get("/users", null, (req: XMLHttpRequest) => { + if (req.readyState == 4 && req.status == 200) { + // same method as inviteList.reload() + let accountsOnDOM: { [id: string]: boolean } = {}; + for (let id in this._users) { accountsOnDOM[id] = true; } + for (let u of (req.response["users"] as User[])) { + if (u.id in this._users) { + this._users[u.id].update(u); + delete accountsOnDOM[u.id]; + } else { + this.add(u); + } + } + for (let id in accountsOnDOM) { + this._users[id].remove(); + delete this._users[id]; + } + this._checkCheckCount; + } + }) +} diff --git a/ts/modules/common.ts b/ts/modules/common.ts index 0ba7821..7dfe918 100644 --- a/ts/modules/common.ts +++ b/ts/modules/common.ts @@ -124,6 +124,7 @@ export function toClipboard (str: string) { export class notificationBox implements NotificationBox { private _box: HTMLDivElement; private _errorTypes: { [type: string]: boolean } = {}; + private _positiveTypes: { [type: string]: boolean } = {}; timeout: number; constructor(box: HTMLDivElement, timeout?: number) { this._box = box; this.timeout = timeout || 5; } @@ -139,7 +140,19 @@ export class notificationBox implements NotificationBox { return noti; } - connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go"); } + private _positive = (bold: string, message: string): HTMLElement => { + const noti = document.createElement('aside'); + noti.classList.add("aside", "~positive", "!normal", "mt-half", "notification-positive"); + noti.innerHTML = `${bold} ${message}`; + const closeButton = document.createElement('span') as HTMLSpanElement; + closeButton.classList.add("button", "~positive", "!low", "ml-1"); + closeButton.innerHTML = ``; + closeButton.onclick = () => { this._box.removeChild(noti); }; + noti.appendChild(closeButton); + return noti; + } + + connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go."); } customError = (type: string, message: string) => { this._errorTypes[type] = this._errorTypes[type] || false; @@ -153,6 +166,19 @@ export class notificationBox implements NotificationBox { this._errorTypes[type] = true; setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._errorTypes[type] = false; } }, this.timeout*1000); } + + customPositive = (type: string, bold: string, message: string) => { + this._positiveTypes[type] = this._positiveTypes[type] || false; + const noti = this._positive(bold, message); + noti.classList.add("positive-" + type); + const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.positive-" + type); + if (this._positiveTypes[type] && previousNoti !== undefined && previousNoti != null) { + previousNoti.remove(); + } + this._box.appendChild(noti); + this._positiveTypes[type] = true; + setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000); + } } export const whichAnimationEvent = () => { @@ -162,3 +188,18 @@ export const whichAnimationEvent = () => { } return "webkitAnimationEnd"; } + +export function toggleLoader(el: HTMLElement, small: boolean = true) { + if (el.classList.contains("loader")) { + el.classList.remove("loader"); + el.classList.remove("loader-sm"); + const dot = el.querySelector("span.dot"); + if (dot) { dot.remove(); } + } else { + el.classList.add("loader"); + if (small) { el.classList.add("loader-sm"); } + const dot = document.createElement("span") as HTMLSpanElement; + dot.classList.add("dot") + el.appendChild(dot); + } +} diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts index dcaf8a9..abe31aa 100644 --- a/ts/modules/invites.ts +++ b/ts/modules/invites.ts @@ -1,4 +1,4 @@ -import { _get, _post, _delete, toClipboard } from "../modules/common.js"; +import { _get, _post, _delete, toClipboard, toggleLoader } from "../modules/common.js"; export class DOMInvite implements Invite { // TODO @@ -390,8 +390,10 @@ export class inviteList implements inviteList { reload = () => _get("/invites", null, (req: XMLHttpRequest) => { if (req.readyState == 4) { let data = req.response; - window.availableProfiles = data["profiles"]; - document.dispatchEvent(this._profileLoadEvent); + if (req.status == 200) { + window.availableProfiles = data["profiles"]; + document.dispatchEvent(this._profileLoadEvent); + } if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) { this.empty = true; return; @@ -446,8 +448,13 @@ export class createInvite { private _uses = document.getElementById('create-uses') as HTMLInputElement; private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement; private _infUsesWarning = document.getElementById('create-inf-uses-warning') as HTMLParagraphElement; - private _createButton = document.getElementById("create-submit") as HTMLInputElement; // Actually a but this allows "disabled" + private _createButton = document.getElementById("create-submit") as HTMLSpanElement; private _profile = document.getElementById("create-profile") as HTMLSelectElement; + + private _days = document.getElementById("create-days") as HTMLSelectElement; + private _hours = document.getElementById("create-hours") as HTMLSelectElement; + private _minutes = document.getElementById("create-minutes") as HTMLSelectElement; + // Broadcast when new invite created private _newInviteEvent = new CustomEvent("newInviteEvent"); private _firstLoad = true; @@ -503,28 +510,34 @@ export class createInvite { set uses(n: number) { this._uses.valueAsNumber = n; } private _checkDurationValidity = () => { - this._createButton.disabled = (this.days + this.hours + this.minutes == 0); + if (this.days + this.hours + this.minutes == 0) { + this._createButton.setAttribute("disabled", ""); + this._createButton.onclick = null; + } else { + this._createButton.removeAttribute("disabled"); + this._createButton.onclick = this.create; + } } get days(): number { - return +(document.getElementById("create-days") as HTMLSelectElement).value; + return +this._days.value; } set days(n: number) { - (document.getElementById("create-days") as HTMLSelectElement).value = ""+n; + this._days.value = ""+n; this._checkDurationValidity(); } get hours(): number { - return +(document.getElementById("create-hours") as HTMLSelectElement).value; + return +this._hours.value; } set hours(n: number) { - (document.getElementById("create-hours") as HTMLSelectElement).value = ""+n; + this._hours.value = ""+n; this._checkDurationValidity(); } get minutes(): number { - return +(document.getElementById("create-minutes") as HTMLSelectElement).value; + return +this._minutes.value; } set minutes(n: number) { - (document.getElementById("create-minutes") as HTMLSelectElement).value = ""+n; + this._minutes.value = ""+n; this._checkDurationValidity(); } @@ -559,6 +572,7 @@ export class createInvite { } create = () => { + toggleLoader(this._createButton); let send = { "days": this.days, "hours": this.hours, @@ -570,8 +584,11 @@ export class createInvite { "profile": this.profile }; _post("/invites", send, (req: XMLHttpRequest) => { - if (req.readyState == 4 && (req.status == 200 || req.status == 204)) { - document.dispatchEvent(this._newInviteEvent); + if (req.readyState == 4) { + if (req.status == 200 || req.status == 204) { + document.dispatchEvent(this._newInviteEvent); + } + toggleLoader(this._createButton); } }); } @@ -588,7 +605,15 @@ export class createInvite { this._createButton.onclick = this.create; this.sendTo = ""; this.uses = 1; + + this._days.onchange = this._checkDurationValidity; + this._hours.onchange = this._checkDurationValidity; + this._minutes.onchange = this._checkDurationValidity; document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false); + + if (!window.emailEnabled) { + document.getElementById("create-send-to-container").classList.add("unfocused"); + } } } diff --git a/ts/modules/tabs.ts b/ts/modules/tabs.ts index c2f60db..09ae14b 100644 --- a/ts/modules/tabs.ts +++ b/ts/modules/tabs.ts @@ -20,14 +20,14 @@ export class Tabs implements Tabs { for (let t of this.tabs) { if (t.tabID == tabID) { t.buttonEl.classList.add("active", "~urge"); - t.preFunc(); + if (t.preFunc) { t.preFunc(); } t.tabEl.classList.remove("unfocused"); + if (t.postFunc) { t.postFunc(); } } else { t.buttonEl.classList.remove("active"); t.buttonEl.classList.remove("~urge"); t.tabEl.classList.add("unfocused"); } - t.postFunc(); } } } diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 9442ad7..1d975da 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -16,7 +16,10 @@ declare interface Window { cssFile: string; availableProfiles: string[]; jfUsers: Array; - notifications_enabled: boolean; + notificationsEnabled: boolean; + emailEnabled: boolean; + ombiEnabled: boolean; + usernameEnabled: boolean; token: string; buttonWidth: number; transitionEvent: string; @@ -29,6 +32,7 @@ declare interface Window { declare interface NotificationBox { connectionError: () => void; customError: (type: string, message: string) => void; + customPositive: (type: string, bold: string, message: string) => void; } declare interface Tabs { @@ -77,5 +81,9 @@ interface inviteList { reload: () => void; } +declare interface SubmitEvent extends Event { + submitter: HTMLInputElement; +} + declare var config: Object; declare var modifiedConfig: Object;