+
-
+
-
-
+
-
+
-
+
-
+
diff --git a/ts/admin.ts b/ts/admin.ts
index ed601d9..05f96a3 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -1,74 +1,24 @@
import { toggleTheme, loadTheme } from "./modules/theme.js";
import { Modal } from "./modules/modal.js";
import { Tabs } from "./modules/tabs.js";
-import { inviteList } from "./modules/invites.js";
-import { notificationBox } from "./modules/common.js";
+import { inviteList, createInvite } from "./modules/invites.js";
+import { _post, notificationBox, whichAnimationEvent } from "./modules/common.js";
loadTheme();
(document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme;
-const whichAnimationEvent = () => {
- const el = document.createElement("fakeElement");
- if (el.style["animation"] !== void 0) {
- return "animationend";
- }
- return "webkitAnimationEnd";
-}
window.animationEvent = whichAnimationEvent();
+window.token = "";
+
+window.availableProfiles = window.availableProfiles || [];
+
+var inviteCreator = new createInvite();
+
+window.invites = new inviteList();
+
window.notifications = new notificationBox(document.getElementById('notification-box') as HTMLDivElement, 5);
-const checkInfUses = function (check: HTMLInputElement, mode = 2) {
- const uses = document.getElementById('inv-uses') as HTMLInputElement;
- if (mode == 2) {
- uses.disabled = check.checked;
- check.parentElement.classList.toggle('~neutral');
- check.parentElement.classList.toggle('~urge');
- check.parentElement.parentElement.nextElementSibling.classList.toggle('unfocused');
- } else if (mode == 1) {
- uses.disabled = true;
- check.checked = true;
- check.parentElement.classList.remove('~neutral');
- check.parentElement.classList.add('~urge');
- check.parentElement.parentElement.nextElementSibling.classList.remove('unfocused');
- } else {
- uses.disabled = false;
- check.checked = false;
- check.parentElement.classList.remove('~urge');
- check.parentElement.classList.add('~neutral');
- check.parentElement.parentElement.nextElementSibling.classList.add('unfocused');
- }
-};
-
-let invInfUses = document.getElementById('inv-inf-uses') as HTMLInputElement;
-invInfUses.onclick = () => { checkInfUses(invInfUses, 2); };
-
-const checkEmailEnabled = function (check: HTMLInputElement, mode = 2) {
- const input = document.getElementById('inv-email') as HTMLInputElement;
- if (mode == 2) {
- input.disabled = !check.checked;
- check.parentElement.classList.toggle('~neutral');
- check.parentElement.classList.toggle('~urge');
- } else if (mode == 1) {
- input.disabled = false;
- check.checked = true;
- check.parentElement.classList.remove('~neutral');
- check.parentElement.classList.add('~urge');
- } else {
- input.disabled = true;
- check.checked = false;
- check.parentElement.classList.remove('~urge');
- check.parentElement.classList.add('~neutral');
- }
-};
-
-let invEmailEnabled = document.getElementById('inv-email-enabled') as HTMLInputElement;
-invEmailEnabled.onchange = () => { checkEmailEnabled(invEmailEnabled, 2); };
-
-checkInfUses(invInfUses, 0);
-checkEmailEnabled(invEmailEnabled, 0);
-
-
const loadAccounts = function () {
const rows: HTMLTableRowElement[] = Array.from(document.getElementById("accounts-list").children);
@@ -175,7 +125,6 @@ window.tabs.addTab("accountsTab", loadAccounts);
window.modals = {} as Modals;
window.modals.login = new Modal(document.getElementById('modal-login'), true);
- document.getElementById('modalButton').onclick = window.modals.login.toggle;
window.modals.addUser = new Modal(document.getElementById('modal-add-user'));
(document.getElementById('accounts-add-user') as HTMLSpanElement).onclick = window.modals.addUser.toggle;
@@ -200,15 +149,6 @@ window.tabs.addTab("accountsTab", loadAccounts);
document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiDefaults.close);
})();
-
-function errorMessage(aside: HTMLElement, content: string, timeout: number = 4) {
- aside.textContent = content;
- aside.classList.remove("unfocused");
- setTimeout(() => { aside.classList.add("unfocused"); }, timeout*1000);
-}
-
-window.token = "";
-
function login(username: string, password: string) {
const req = new XMLHttpRequest();
req.responseType = 'json';
@@ -234,8 +174,7 @@ function login(username: string, password: string) {
errorMsg = "Unknown error";
}
if (!refresh) {
- errorMessage(window.modals.login.modal.querySelector("aside"), errorMsg, 5);
-
+ window.notifications.customError("loginError", errorMsg);
} else {
window.modals.login.show();
}
@@ -245,6 +184,8 @@ function login(username: string, password: string) {
window.modals.login.close();
window.invites.reload();
setInterval(window.invites.reload, 30*1000);
+ document.getElementById("logout-button").classList.remove("unfocused");
+
/*generateInvites();
setInterval((): void => generateInvites(), 60 * 1000);
addOptions(30, document.getElementById('days') as HTMLSelectElement);
@@ -272,12 +213,18 @@ document.getElementById('form-login').addEventListener('submit', (event: Event)
const username = (document.getElementById("login-user") as HTMLInputElement).value;
const password = (document.getElementById("login-password") as HTMLInputElement).value;
if (!username || !password) {
- errorMessage(window.modals.login.modal.querySelector("aside"), "The username and/or password were left blank." , 4);
+ window.notifications.customError("loginError", "The username and/or password were left blank.");
return;
}
- login(username, password, true);
+ login(username, password);
});
login("", "");
-window.invites = new inviteList();
+(document.getElementById('logout-button') as HTMLButtonElement).onclick = () => _post("/logout", null, (req: XMLHttpRequest): boolean => {
+ if (req.readyState == 4 && req.status == 200) {
+ window.token = "";
+ location.reload();
+ return false;
+ }
+});
diff --git a/ts/modules/common.ts b/ts/modules/common.ts
index 3a1937f..0ba7821 100644
--- a/ts/modules/common.ts
+++ b/ts/modules/common.ts
@@ -49,14 +49,21 @@ export const rmAttr = (el: HTMLElement, attr: string): void => {
};
export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
-
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => {
let req = new XMLHttpRequest();
req.open("GET", window.URLBase + url, true);
req.responseType = 'json';
req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
- req.onreadystatechange = () => { if (req.status == 0) { window.notifications.connectionError(); } else { onreadystatechange(req); } };
+ req.onreadystatechange = () => {
+ if (req.status == 0) {
+ window.notifications.connectionError();
+ return;
+ } else if (req.status == 401) {
+ window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
+ }
+ onreadystatechange(req);
+ };
req.send(JSON.stringify(data));
};
@@ -68,7 +75,15 @@ export const _post = (url: string, data: Object, onreadystatechange: (req: XMLHt
}
req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
- req.onreadystatechange = () => { if (req.status == 0) { window.notifications.connectionError(); } else { onreadystatechange(req); } };
+ req.onreadystatechange = () => {
+ if (req.status == 0) {
+ window.notifications.connectionError();
+ return;
+ } else if (req.status == 401) {
+ window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
+ }
+ onreadystatechange(req);
+ };
req.send(JSON.stringify(data));
};
@@ -77,7 +92,15 @@ export function _delete(url: string, data: Object, onreadystatechange: (req: XML
req.open("DELETE", window.URLBase + url, true);
req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
- req.onreadystatechange = () => { if (req.status == 0) { window.notifications.connectionError(); } else { onreadystatechange(req); } };
+ req.onreadystatechange = () => {
+ if (req.status == 0) {
+ window.notifications.connectionError();
+ return;
+ } else if (req.status == 401) {
+ window.notifications.customError("401Error", "Unauthorized. Try logging back in.");
+ }
+ onreadystatechange(req);
+ };
req.send(JSON.stringify(data));
}
@@ -100,7 +123,8 @@ export function toClipboard (str: string) {
export class notificationBox implements NotificationBox {
private _box: HTMLDivElement;
- timeout: number
+ private _errorTypes: { [type: string]: boolean } = {};
+ timeout: number;
constructor(box: HTMLDivElement, timeout?: number) { this._box = box; this.timeout = timeout || 5; }
private _error = (message: string): HTMLElement => {
@@ -115,14 +139,26 @@ export class notificationBox implements NotificationBox {
return noti;
}
- private _connectionError: boolean = false;
- connectionError = () => {
- const noti = this._error("Couldn't connect to jfa-go.");
- if (this._connectionError) {
- this._box.querySelector("aside.notification-error").remove();
+ connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go"); }
+
+ customError = (type: string, message: string) => {
+ this._errorTypes[type] = this._errorTypes[type] || false;
+ const noti = this._error(message);
+ noti.classList.add("error-" + type);
+ const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.error-" + type);
+ if (this._errorTypes[type] && previousNoti !== undefined && previousNoti != null) {
+ previousNoti.remove();
}
this._box.appendChild(noti);
- this._connectionError = true;
- setTimeout(() => { this._box.removeChild(noti); this._connectionError = false; }, this.timeout*1000);
+ this._errorTypes[type] = true;
+ setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._errorTypes[type] = false; } }, this.timeout*1000);
}
}
+
+export const whichAnimationEvent = () => {
+ const el = document.createElement("fakeElement");
+ if (el.style["animation"] !== void 0) {
+ return "animationend";
+ }
+ return "webkitAnimationEnd";
+}
diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts
index 112def4..dcaf8a9 100644
--- a/ts/modules/invites.ts
+++ b/ts/modules/invites.ts
@@ -1,6 +1,9 @@
import { _get, _post, _delete, toClipboard } from "../modules/common.js";
export class DOMInvite implements Invite {
+ // TODO
+ // add setProfile
+ //
updateNotify = (checkbox: HTMLInputElement) => {
let state: { [code: string]: { [type: string]: boolean } } = {};
let revertChanges: () => void;
@@ -18,11 +21,13 @@ export class DOMInvite implements Invite {
});
}
- delete = () => { _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => {
+ delete = () => _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => {
if (req.readyState == 4 && (req.status == 200 || req.status == 204)) {
this.remove();
+ const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { "detail": this.code });
+ document.dispatchEvent(inviteDeletedEvent);
}
- }); }
+ })
private _code: string = "None";
get code(): string { return this._code; }
@@ -153,7 +158,23 @@ export class DOMInvite implements Invite {
innerHTML += `
`;
}
select.innerHTML = innerHTML;
+ this._profile = selected;
};
+ updateProfile = () => {
+ const select = this._left.querySelector("select") as HTMLSelectElement;
+ const previous = this.profile;
+ let profile = select.value;
+ if (profile == "noProfile") { profile = ""; }
+ _post("/invites/profile", { "invite": this.code, "profile": profile }, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ if (!(req.status == 200 || req.status == 204)) {
+ select.value = previous || "noProfile";
+ } else {
+ this._profile = profile;
+ }
+ }
+ });
+ }
private _container: HTMLDivElement;
@@ -202,7 +223,21 @@ export class DOMInvite implements Invite {
`;
- (this._codeArea.querySelector("span.button") as HTMLSpanElement).onclick = () => { toClipboard(this._codeLink); };
+ const copyButton = this._codeArea.querySelector("span.button") as HTMLSpanElement;
+ copyButton.onclick = () => {
+ toClipboard(this._codeLink);
+ const icon = copyButton.children[0];
+ icon.classList.remove("ri-file-copy-line");
+ icon.classList.add("ri-check-line");
+ copyButton.classList.remove("~info");
+ copyButton.classList.add("~positive");
+ setTimeout(() => {
+ icon.classList.remove("ri-check-line");
+ icon.classList.add("ri-file-copy-line");
+ copyButton.classList.remove("~positive");
+ copyButton.classList.add("~info");
+ }, 800);
+ };
this._infoArea = document.createElement('div') as HTMLDivElement;
this._header.appendChild(this._infoArea);
@@ -252,6 +287,8 @@ export class DOMInvite implements Invite {
On user creation
`;
+ (this._left.querySelector("select") as HTMLSelectElement).onchange = this.updateProfile;
+
const notifyExpiry = this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement;
notifyExpiry.onchange = () => { this._notifyExpiry = notifyExpiry.checked; this.updateNotify(notifyExpiry); };
@@ -276,6 +313,8 @@ export class DOMInvite implements Invite {
this.expanded = false;
this.update(invite);
+
+ document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
}
update = (invite: Invite) => {
@@ -295,19 +334,27 @@ export class DOMInvite implements Invite {
remove = () => { this._container.remove(); }
}
-// TODO:
-// implement inviteList as a class
-// inviteList has empty boolean value, set true adds an emptyInvite
-
export class inviteList implements inviteList {
private _list: HTMLDivElement;
private _empty: boolean;
+ // since invite reload sends profiles, this event it broadcast so the createInvite object can load them.
+ private _profileLoadEvent = new CustomEvent("profileLoadEvent");
+
invites: { [code: string]: DOMInvite };
constructor() {
this._list = document.getElementById('invites') as HTMLDivElement;
this.empty = true;
this.invites = {};
+ document.addEventListener("newInviteEvent", () => { this.reload(); }, false);
+ document.addEventListener("inviteDeletedEvent", (event: CustomEvent) => {
+ const code = event.detail;
+ const length = Object.keys(this.invites).length - 1; // store prior as Object.keys is undefined when there are no keys
+ delete this.invites[code];
+ if (length == 0) {
+ this.empty = true;
+ }
+ }, false);
}
get empty(): boolean { return this._empty; }
@@ -340,14 +387,12 @@ export class inviteList implements inviteList {
this._list.appendChild(domInv.asElement());
}
- reload = () => { _get("/invites", null, (req: XMLHttpRequest) => {
+ reload = () => _get("/invites", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
let data = req.response;
window.availableProfiles = data["profiles"];
- for (let code in this.invites) {
- this.invites[code].loadProfiles();
- }
- if (data["invites"] === undefined || data["invites"].length == 0) {
+ document.dispatchEvent(this._profileLoadEvent);
+ if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) {
this.empty = true;
return;
}
@@ -370,7 +415,7 @@ export class inviteList implements inviteList {
delete this.invites[code];
}
}
- }) }
+ })
}
@@ -394,66 +439,158 @@ function parseInvite(invite: { [f: string]: string | number | string[][] | boole
parsed.notifyCreation = invite["notify-creation"] as boolean || false;
return parsed;
}
+
+export class createInvite {
+ private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
+ private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
+ 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 _profile = document.getElementById("create-profile") as HTMLSelectElement;
+ // Broadcast when new invite created
+ private _newInviteEvent = new CustomEvent("newInviteEvent");
+ private _firstLoad = true;
+
+ private _count: Number = 30;
+ private _populateNumbers = () => {
+ const fieldIDs = ["create-days", "create-hours", "create-minutes"];
+ for (let i = 0; i < fieldIDs.length; i++) {
+ const field = document.getElementById(fieldIDs[i]);
+ field.textContent = '';
+ for (let n = 0; n <= this._count; n++) {
+ const opt = document.createElement("option") as HTMLOptionElement;
+ opt.textContent = ""+n;
+ opt.value = ""+n;
+ field.appendChild(opt);
+ }
+ }
+ }
+
+ get sendToEnabled(): boolean {
+ return this._sendToEnabled.checked;
+ }
+ set sendToEnabled(state: boolean) {
+ this._sendToEnabled.checked = state;
+ this._sendTo.disabled = !state;
+ if (state) {
+ this._sendToEnabled.parentElement.classList.remove("~neutral");
+ this._sendToEnabled.parentElement.classList.add("~urge");
+ } else {
+ this._sendToEnabled.parentElement.classList.remove("~urge");
+ this._sendToEnabled.parentElement.classList.add("~neutral");
+ }
+ }
+
+ get infiniteUses(): boolean {
+ return this._infUses.checked;
+ }
+ set infiniteUses(state: boolean) {
+ this._infUses.checked = state;
+ this._uses.disabled = state;
+ if (state) {
+ this._infUses.parentElement.classList.remove("~neutral");
+ this._infUses.parentElement.classList.add("~urge");
+ this._infUsesWarning.classList.remove("unfocused");
+ } else {
+ this._infUses.parentElement.classList.remove("~urge");
+ this._infUses.parentElement.classList.add("~neutral");
+ this._infUsesWarning.classList.add("unfocused");
+ }
+ }
+ get uses(): number { return this._uses.valueAsNumber; }
+ set uses(n: number) { this._uses.valueAsNumber = n; }
+ private _checkDurationValidity = () => {
+ this._createButton.disabled = (this.days + this.hours + this.minutes == 0);
+ }
-/*
-function addInvite(invite: Invite): void {
- const list = document.getElementById('invites') as HTMLDivElement;
- const container = document.createElement('div') as HTMLDivElement;
- container.classList.add("inv");
- container.id = "invite-" + invite.code;
- // invite header
- const header = document.createElement("div") as HTMLDivElement;
- (() => {
- header.classList.add("card", "~neutral", "!normal", "inv-header", "flex-expand", "mt-half");
- // code area (code, copy button, "sent to" message)
- const codeArea = document.createElement('div') as HTMLDivElement;
- (() => {
- codeArea.classList.add('inv-codearea');
- if (invite.empty) {
- codeArea.innerHTML = `
- None
- `;
- return;
+ get days(): number {
+ return +(document.getElementById("create-days") as HTMLSelectElement).value;
+ }
+ set days(n: number) {
+ (document.getElementById("create-days") as HTMLSelectElement).value = ""+n;
+ this._checkDurationValidity();
+ }
+ get hours(): number {
+ return +(document.getElementById("create-hours") as HTMLSelectElement).value;
+ }
+ set hours(n: number) {
+ (document.getElementById("create-hours") as HTMLSelectElement).value = ""+n;
+ this._checkDurationValidity();
+ }
+ get minutes(): number {
+ return +(document.getElementById("create-minutes") as HTMLSelectElement).value;
+ }
+ set minutes(n: number) {
+ (document.getElementById("create-minutes") as HTMLSelectElement).value = ""+n;
+ this._checkDurationValidity();
+ }
+
+ get sendTo(): string { return this._sendTo.value; }
+ set sendTo(address: string) { this._sendTo.value = address; }
+
+ get profile(): string {
+ const val = this._profile.value;
+ if (val == "noProfile") {
+ return "";
+ }
+ return val;
+ }
+ set profile(p: string) {
+ if (p == "") { p = "noProfile"; }
+ this._profile.value = p;
+ }
+
+ loadProfiles = () => {
+ let innerHTML = ``;
+ for (let profile of window.availableProfiles) {
+ innerHTML += ``;
+ }
+ let selected = this.profile;
+ this._profile.innerHTML = innerHTML;
+ if (this._firstLoad) {
+ this.profile = window.availableProfiles[0] || "";
+ this._firstLoad = false;
+ } else {
+ this.profile = selected;
+ }
+ }
+
+ create = () => {
+ let send = {
+ "days": this.days,
+ "hours": this.hours,
+ "minutes": this.minutes,
+ "multiple-uses": (this.uses > 1 || this.infiniteUses),
+ "no-limit": this.infiniteUses,
+ "remaining-uses": this.uses,
+ "email": this.sendToEnabled ? this.sendTo : "",
+ "profile": this.profile
+ };
+ _post("/invites", send, (req: XMLHttpRequest) => {
+ if (req.readyState == 4 && (req.status == 200 || req.status == 204)) {
+ document.dispatchEvent(this._newInviteEvent);
}
- const link = window.location.href.split("#")[0] + "invite/" + invite.code;
- let innerHTML = `
- ${invite.code.replace(/-/g, '-')}
-
- `;
- if (invite.email) {
- let email = invite.email;
- if (!invite.email.includes("Failed to send to")) {
- email = "Sent to " + email;
- }
- innerHTML += `
- ${email}
- `;
- }
- codeArea.innerHTML = innerHTML;
- })();
- header.appendChild(codeArea);
+ });
+ }
- // info area (expiry, delete, dropdown button)
- const infoArea = document.createElement('div') as HTMLDivElement;
- (() => {
- infoArea.classList.add("inv-infoarea");
- infoArea.innerHTML = `
- ${invite.expiresIn}
- Delete
-