+
An example tooltip.
diff --git a/ts/admin.ts b/ts/admin.ts
index be0e53c..b4947e9 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -1,6 +1,7 @@
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";
loadTheme();
(document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme;
@@ -13,7 +14,7 @@ const whichAnimationEvent = () => {
return "webkitAnimationEnd";
}
window.animationEvent = whichAnimationEvent();
-const toggles: HTMLInputElement[] = Array.from(document.getElementsByClassName('toggle-details'));
+/*const toggles: HTMLInputElement[] = Array.from(document.getElementsByClassName('toggle-details'));
for (let toggle of toggles) {
toggle.onclick = () => {
const el = toggle.parentElement.parentElement.parentElement.nextElementSibling as HTMLDivElement;
@@ -27,7 +28,7 @@ for (let toggle of toggles) {
toggle.previousElementSibling.classList.toggle("rotated");
toggle.previousElementSibling.classList.toggle("not-rotated");
};
-}
+}*/
const checkInfUses = function (check: HTMLInputElement, mode = 2) {
const uses = document.getElementById('inv-uses') as HTMLInputElement;
@@ -254,6 +255,8 @@ function login(username: string, password: string) {
const data = this.response;
window.token = data["token"];
window.modals.login.close();
+ window.invites.reload();
+ setInterval(window.invites.reload, 30*1000);
/*generateInvites();
setInterval((): void => generateInvites(), 60 * 1000);
addOptions(30, document.getElementById('days') as HTMLSelectElement);
@@ -288,3 +291,5 @@ document.getElementById('form-login').addEventListener('submit', (event: Event)
});
login("", "");
+
+window.invites = new inviteList();
diff --git a/ts/modules/common.ts b/ts/modules/common.ts
index 39ba0d2..a7a0041 100644
--- a/ts/modules/common.ts
+++ b/ts/modules/common.ts
@@ -50,13 +50,13 @@ 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: () => void): void => {
+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 = onreadystatechange;
+ req.onreadystatechange = () => { onreadystatechange(req); };
req.send(JSON.stringify(data));
};
@@ -81,3 +81,19 @@ export function _delete(url: string, data: Object, onreadystatechange: () => voi
req.send(JSON.stringify(data));
}
+export function toClipboard (str: string) {
+ const el = document.createElement('textarea') as HTMLTextAreaElement;
+ el.value = str;
+ el.readOnly = true;
+ el.style.position = "absolute";
+ el.style.left = "-9999px";
+ document.body.appendChild(el);
+ const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
+ el.select();
+ document.execCommand("copy");
+ document.body.removeChild(el);
+ if (selected) {
+ document.getSelection().removeAllRanges();
+ document.getSelection().addRange(selected);
+ }
+}
diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts
new file mode 100644
index 0000000..39a57ca
--- /dev/null
+++ b/ts/modules/invites.ts
@@ -0,0 +1,436 @@
+import { _get, _post, _delete, toClipboard } from "../modules/common.js";
+
+export class DOMInvite implements Invite {
+
+ // TODO
+ updateNotify = () => {}; // SetNotify
+ delete = () => {}; // deleteInvite
+
+ private _code: string = "None";
+ get code(): string { return this._code; }
+ set code(code: string) {
+ this._code = code;
+ this._codeLink = window.location.href.split("#")[0] + "invite/" + code;
+ const linkEl = this._codeArea.querySelector("a") as HTMLAnchorElement;
+ linkEl.textContent = code.replace(/-/g, '-');
+ linkEl.href = this._codeLink;
+ }
+ private _codeLink: string;
+
+ private _expiresIn: string;
+ get expiresIn(): string { return this._expiresIn }
+ set expiresIn(expiry: string) {
+ this._expiresIn = expiry;
+ this._infoArea.querySelector("span.inv-expiry").textContent = expiry;
+ }
+
+ private _remainingUses: string = "1";
+ get remainingUses(): string { return this._remainingUses; }
+ set remainingUses(remaining: string) {
+ this._remainingUses = remaining;
+ this._middle.querySelector("strong.inv-remaining").textContent = remaining;
+ }
+
+ private _email: string = "";
+ get email(): string { return this._email };
+ set email(address: string) {
+ this._email = address;
+ const icon = this._infoArea.querySelector(".tooltip i");
+ const chip = this._infoArea.querySelector(".tooltip span.inv-email-chip");
+ const tooltip = this._infoArea.querySelector(".tooltip span.content") as HTMLSpanElement;
+ if (address == "") {
+ icon.classList.remove("ri-mail-line");
+ icon.classList.remove("ri-mail-close-line");
+ chip.classList.remove("~neutral");
+ chip.classList.remove("~critical");
+ chip.classList.remove("chip");
+ } else {
+ chip.classList.add("chip");
+ if (address.includes("Failed to send to")) {
+ icon.classList.remove("ri-mail-line");
+ icon.classList.add("ri-mail-close-line");
+ chip.classList.remove("~neutral");
+ chip.classList.add("~critical");
+ } else {
+ address = "Sent to " + address;
+ icon.classList.remove("ri-mail-close-line");
+ icon.classList.add("ri-mail-line");
+ chip.classList.remove("~critical");
+ chip.classList.add("~neutral");
+ }
+ }
+ tooltip.textContent = address;
+ }
+
+ private _usedBy: string[][];
+ get usedBy(): string[][] { return this._usedBy; }
+ set usedBy(uB: string[][]) {
+ // ub[i][0]: username, ub[i][1]: date
+ this._usedBy = uB;
+ if (uB.length == 0) {
+ this._right.classList.add("empty");
+ this._userTable.innerHTML = `
None yet!
`;
+ return;
+ }
+ this._right.classList.remove("empty");
+ let innerHTML = `
+
+
+
+ Name |
+ Date |
+
+
+
+ `;
+ for (let user of uB) {
+ innerHTML += `
+
+ ${user[0]} |
+ ${user[1]} |
+
+ `;
+ }
+ innerHTML += `
+
+
+ `;
+ this._userTable.innerHTML = innerHTML;
+ }
+
+ private _created: string;
+ get created(): string { return this._created; }
+ set created(created: string) {
+ this._created = created;
+ this._middle.querySelector("strong.inv-created").textContent = created;
+ }
+
+ private _notifyExpiry: boolean = false;
+ get notifyExpiry(): boolean { return this._notifyExpiry }
+ set notifyExpiry(state: boolean) {
+ this._notifyExpiry = state;
+ (this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement).checked = state;
+ }
+
+ private _notifyCreation: boolean = false;
+ get notifyCreation(): boolean { return this._notifyCreation }
+ set notifyCreation(state: boolean) {
+ this._notifyCreation = state;
+ (this._left.querySelector("input.inv-notify-creation") as HTMLInputElement).checked = state;
+ }
+
+ private _profile: string;
+ get profile(): string { return this._profile; }
+ set profile(profile: string) { this.loadProfiles(profile); }
+ loadProfiles = (selected?: string) => {
+ const select = this._left.querySelector("select") as HTMLSelectElement;
+ let noProfile = false;
+ if (selected === "") {
+ noProfile = true;
+ } else {
+ selected = selected || select.value;
+ }
+ let innerHTML = `
`;
+ for (let profile of window.availableProfiles) {
+ innerHTML += `
`;
+ }
+ select.innerHTML = innerHTML;
+ };
+
+ private _container: HTMLDivElement;
+
+ private _header: HTMLDivElement;
+ private _codeArea: HTMLDivElement;
+ private _infoArea: HTMLDivElement;
+
+ private _details: HTMLDivElement;
+ private _left: HTMLDivElement;
+ private _middle: HTMLDivElement;
+ private _right: HTMLDivElement;
+ private _userTable: HTMLDivElement;
+
+ // whether the details card is expanded.
+ get expanded(): boolean {
+ return this._details.classList.contains("focused");
+ }
+ set expanded(state: boolean) {
+ const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
+ if (state) {
+ this._details.classList.remove("unfocused");
+ this._details.classList.add("focused");
+ toggle.previousElementSibling.classList.add("rotated");
+ toggle.previousElementSibling.classList.remove("not-rotated");
+ } else {
+ this._details.classList.add("unfocused");
+ this._details.classList.remove("focused");
+ toggle.previousElementSibling.classList.remove("rotated");
+ toggle.previousElementSibling.classList.add("not-rotated");
+ }
+ }
+
+ constructor(invite: Invite) {
+ // first create the invite structure, then use our setter methods to fill in the data.
+ this._container = document.createElement('div') as HTMLDivElement;
+ this._container.classList.add("inv");
+
+ this._header = document.createElement('div') as HTMLDivElement;
+ this._container.appendChild(this._header);
+ this._header.classList.add("card", "~neutral", "!normal", "inv-header", "flex-expand", "mt-half");
+
+ this._codeArea = document.createElement('div') as HTMLDivElement;
+ this._header.appendChild(this._codeArea);
+ this._codeArea.classList.add("inv-codearea");
+ this._codeArea.innerHTML = `
+
+
+ `;
+ (this._codeArea.querySelector("span.button") as HTMLSpanElement).onclick = () => { toClipboard(this._codeLink); };
+
+ this._infoArea = document.createElement('div') as HTMLDivElement;
+ this._header.appendChild(this._infoArea);
+ this._infoArea.classList.add("inv-infoarea");
+ this._infoArea.innerHTML = `
+
+
+
+
+
+
Delete
+
+ `;
+
+ (this._infoArea.querySelector(".inv-delete") as HTMLSpanElement).onclick = this.delete;
+
+ const toggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
+ toggle.onchange = () => { this.expanded = !this.expanded; };
+
+ this._details = document.createElement('div') as HTMLDivElement;
+ this._container.appendChild(this._details);
+ this._details.classList.add("card", "~neutral", "!normal", "mt-half", "inv-details");
+ const detailsInner = document.createElement('div') as HTMLDivElement;
+ this._details.appendChild(detailsInner);
+ detailsInner.classList.add("inv-row", "flex-expand", "align-top");
+
+ this._left = document.createElement('div') as HTMLDivElement;
+ detailsInner.appendChild(this._left);
+ this._left.classList.add("inv-profilearea");
+ this._left.innerHTML = `
+
Profile
+
+
+
+
Notify on:
+
+
+ `;
+
+ this._middle = document.createElement('div') as HTMLDivElement;
+ detailsInner.appendChild(this._middle);
+ this._middle.classList.add("block");
+ this._middle.innerHTML = `
+
Created
+
Remaining uses
+ `;
+
+ this._right = document.createElement('div') as HTMLDivElement;
+ detailsInner.appendChild(this._right);
+ this._right.classList.add("card", "~neutral", "!low", "inv-created-users");
+ this._right.innerHTML = ``;
+ this._userTable = document.createElement('div') as HTMLDivElement;
+ this._right.appendChild(this._userTable);
+
+
+ this.expanded = false;
+ this.update(invite);
+ }
+
+ update = (invite: Invite) => {
+ this.code = invite.code;
+ this.created = invite.created;
+ this.email = invite.email;
+ this.expiresIn = invite.expiresIn;
+ this.notifyCreation = invite.notifyCreation;
+ this.notifyExpiry = invite.notifyExpiry;
+ this.profile = invite.profile;
+ this.remainingUses = invite.remainingUses;
+ this.usedBy = invite.usedBy;
+ }
+
+ asElement = (): HTMLDivElement => { return this._container; }
+
+ 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;
+ invites: { [code: string]: DOMInvite };
+
+ constructor() {
+ this._list = document.getElementById('invites') as HTMLDivElement;
+ this.empty = true;
+ this.invites = {};
+ }
+
+ get empty(): boolean { return this._empty; }
+ set empty(state: boolean) {
+ this._empty = state;
+ if (state) {
+ this.invites = {};
+ this._list.classList.add("empty");
+ this._list.innerHTML = `
+
+ `;
+ } else {
+ this._list.classList.remove("empty");
+ if (this._list.querySelector(".inv-empty")) {
+ this._list.textContent = '';
+ }
+ }
+ }
+
+ add = (invite: Invite) => {
+ let domInv = new DOMInvite(invite);
+ this.invites[invite.code] = domInv;
+ if (this.empty) { this.empty = false; }
+ this._list.appendChild(domInv.asElement());
+ }
+
+ 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) {
+ this.empty = true;
+ return;
+ }
+ // get a list of all current inv codes on dom
+ // every time we find a match in resp, delete from list
+ // at end delete all remaining in list from dom
+ let invitesOnDOM: { [code: string]: boolean } = {};
+ for (let code in this.invites) { invitesOnDOM[code] = true; }
+ for (let inv of (data["invites"] as Array
)) {
+ const invite = parseInvite(inv);
+ if (invite.code in this.invites) {
+ this.invites[invite.code].update(invite);
+ delete invitesOnDOM[invite.code];
+ } else {
+ this.add(invite);
+ }
+ }
+ for (let code in invitesOnDOM) {
+ this.invites[code].remove();
+ delete this.invites[code];
+ }
+ }
+ }) }
+}
+
+
+function parseInvite(invite: { [f: string]: string | number | string[][] | boolean }): Invite {
+ let parsed: Invite = {};
+ parsed.code = invite["code"] as string;
+ parsed.email = invite["email"] as string || "";
+ let time = "";
+ const fields = ["days", "hours", "minutes"];
+ for (let i = 0; i < fields.length; i++) {
+ if (invite[fields[i]] != 0) {
+ time += `${invite[fields[i]]}${fields[i][0]} `;
+ }
+ }
+ parsed.expiresIn = `Expires in ${time.slice(0, -1)}`;
+ parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"])
+ parsed.usedBy = invite["used-by"] as string[][] || [];
+ parsed.created = invite["created"] as string || "Unknown";
+ parsed.profile = invite["profile"] as string || "";
+ parsed.notifyExpiry = invite["notify-expiry"] as boolean || false;
+ parsed.notifyCreation = invite["notify-creation"] as boolean || false;
+ return parsed;
+}
+
+
+
+/*
+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;
+ }
+ 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
+