1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-29 12:30:11 +00:00

fully functional invites tab, flesh out bugs

also:
* fix select chevrons appearing above modals
* add custom errors and use them for http errors, also now appear above
  modals.
* functional logout button
* slightly cleaned up admin.ts
This commit is contained in:
Harvey Tindall 2020-12-30 23:48:01 +00:00
parent 28187d0aa0
commit 2d443fb50b
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
7 changed files with 302 additions and 294 deletions

View File

@ -299,4 +299,5 @@ p.top {
position: fixed; position: fixed;
right: 1rem; right: 1rem;
bottom: 1rem; bottom: 1rem;
z-index: 16;
} }

View File

@ -1,7 +1,7 @@
.modal { .modal {
display: none; display: none;
position: fixed; position: fixed;
z-index: 1; z-index: 12;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;

View File

@ -15,7 +15,6 @@
<span class="heading">Login</span> <span class="heading">Login</span>
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="username" id="login-user"> <input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="username" id="login-user">
<input type="password" class="field input ~neutral !high mb-1" placeholder="password" id="login-password"> <input type="password" class="field input ~neutral !high mb-1" placeholder="password" id="login-password">
<aside class="aside sm ~critical mb-half unfocused"></aside>
<input type="submit" class="button ~urge !normal full-width center supra submit" value="Login"> <input type="submit" class="button ~urge !normal full-width center supra submit" value="Login">
</form> </form>
</div> </div>
@ -30,7 +29,7 @@
<div id="modal-about" class="modal"> <div id="modal-about" class="modal">
<div class="modal-content content card"> <div class="modal-content content card">
<span class="heading">About <span class="modal-close">&times;</span></span> <span class="heading">About <span class="modal-close">&times;</span></span>
<img src="images/banner.svg" class="mt-1" alt="jfa-go banner"> <img src="/banner.svg" class="mt-1" alt="jfa-go banner">
<p><i class="icon ri-github-fill"></i><a href="https://github.com/hrfee/jfa-go">jfa-go</a></p> <p><i class="icon ri-github-fill"></i><a href="https://github.com/hrfee/jfa-go">jfa-go</a></p>
<p>Version <span class="code monospace">{{ .version }}</span></p> <p>Version <span class="code monospace">{{ .version }}</span></p>
<p>Commit <span class="code monospace">{{ .commit }}</span></p> <p>Commit <span class="code monospace">{{ .commit }}</span></p>
@ -126,174 +125,61 @@
</div> </div>
<div class="mb-1"> <div class="mb-1">
<div class="text-neutral-700"> <div class="text-neutral-700">
<span class="button ~critical !normal mb-1">Logout</span> <span class="button ~critical !normal mb-1 unfocused" id="logout-button">Logout</span>
<span id="button-theme" class="button ~neutral !normal mb-1">Theme</span> <span id="button-theme" class="button ~neutral !normal mb-1">Theme</span>
<span id="modalButton" class="button ~neutral !normal mb-1">Trigger Login</span>
</div> </div>
</div> </div>
<div id="invitesTab"> <div id="invitesTab">
<div class="card ~neutral !low invites mb-1"> <div class="card ~neutral !low invites mb-1">
<span class="heading">Invites</span> <span class="heading">Invites</span>
<div id="invites"> <div id="invites"></div>
<div class="inv">
<div class="card ~neutral !normal inv-header flex-expand mt-half">
<div class="inv-codearea">
<a href="#" class="code monospace mr-1">ZD8ZeC55Jcpmbtv54FuVM3</a>
<span class="button ~info !normal" title="Copy invite link"><i class="ri-file-copy-line"></i></span>
<span class="support ml-1">Failed to send to this.email@addre.ss</span>
</div> </div>
<div class="inv-infoarea"> <div class="card ~neutral !low">
<span class="inv-expiry mr-1">Expires in 30m</span>
<span class="button ~critical !normal">Delete</span>
<label>
<i class="icon ri-arrow-down-s-line not-rotated"></i>
<input type="checkbox" class="toggle-details unfocused">
</label>
</div>
</div>
<div class="card ~neutral !normal mt-half inv-details mt-half unfocused">
<div class="inv-row flex-expand align-top">
<div class="inv-profilearea">
<p class="supra mb-1 top">Profile</p>
<div class="select ~neutral !normal inv-profileselect inline-block">
<select>
<option>Friends</option>
<option>Family</option>
<option>No Profile</option>
</select>
</div>
<p class="label supra">Notify on:</p>
<label class="switch block">
<input type="checkbox" class="inv-notify-expiry">
<span>On expiry</span>
</label>
<label class="switch block">
<input type="checkbox" class="inv-notify-creation">
<span>On user creation</span>
</label>
</div>
<div class="block">
<p class="supra mb-1 top">Created <strong class="inv-created">10/12/20 18:46</strong></p>
<p class="supra mb-1">Remaining uses <strong class="inv-remaining">8</strong></p>
</div>
<div class="card ~neutral !low inv-created-users">
<strong class="supra table-header">Created users</strong>
<table class="table inv-table">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<tr>
<td>jeff</td>
<td>10/12/20 19:00</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="inv">
<div class="card ~neutral !normal inv-header flex-expand mt-half">
<div class="inv-codearea">
<a href="#" class="code monospace mr-1">ZD8ZeC55Jcpmbtv54FuVM3</a>
<span class="button ~info !normal">Copy</span>
</div>
<div class="inv-infoarea">
<span class="inv-expiry mr-1">Expires in 30m</span>
<span class="button ~critical !normal">Delete</span>
<label>
<i class="icon ri-arrow-down-s-line not-rotated"></i>
<input type="checkbox" class="toggle-details unfocused">
</label>
</div>
</div>
<div class="card ~neutral !normal mt-half inv-details mt-half unfocused">
<div class="inv-row flex-expand align-top">
<div class="inv-profilearea">
<p class="supra mb-1 top">Profile</p>
<div class="select ~neutral !normal inv-profileselect inline-block">
<select>
<option>Friends</option>
<option>Family</option>
<option>No Profile</option>
</select>
</div>
<p class="label supra">Notify on:</p>
<label class="switch block">
<input type="checkbox" class="inv-notify-expiry">
<span>On expiry</span>
</label>
<label class="switch block">
<input type="checkbox" class="inv-notify-creation">
<span>On user creation</span>
</label>
</div>
<div class="block">
<p class="supra mb-1 top">Created <strong class="inv-created">10/12/20 18:46</strong></p>
<p class="supra mb-1">Remaining uses <strong class="inv-remaining">8</strong></p>
</div>
<div class="card ~neutral !low inv-created-users empty">
<strong class="supra table-header">Created users</strong>
<p class="content">None yet!</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card ~neutral !low create-inv">
<span class="heading">Create</span> <span class="heading">Create</span>
<div class="row"> <div class="row" id="create-inv">
<div class="card ~neutral !normal col"> <div class="card ~neutral !normal col">
<label class="label supra" for="inv-days">Days</label> <label class="label supra" for="create-days">Days</label>
<div class="select ~neutral !normal mb-1 mt-half"> <div class="select ~neutral !normal mb-1 mt-half">
<select id="inv-days"> <select id="create-days">
<option>0</option> <option>0</option>
</select> </select>
</div> </div>
<label class="label supra" for="inv-hours">Hours</label> <label class="label supra" for="create-hours">Hours</label>
<div class="select ~neutral !normal mb-1 mt-half"> <div class="select ~neutral !normal mb-1 mt-half">
<select id="inv-hours"> <select id="create-hours">
<option>0</option> <option>0</option>
</select> </select>
</div> </div>
<label class="label supra" for="inv-minutes">Minutes</label> <label class="label supra" for="create-minutes">Minutes</label>
<div class="select ~neutral !normal mb-1 mt-half"> <div class="select ~neutral !normal mb-1 mt-half">
<select id="inv-minutes"> <select id="create-minutes">
<option>0</option> <option>0</option>
</select> </select>
</div> </div>
</div> </div>
<div class="card ~neutral !normal col"> <div class="card ~neutral !normal col">
<label class="label supra" for="inv-uses">Number of uses</label> <label class="label supra" for="create-uses">Number of uses</label>
<div class="flex-expand mb-1 mt-half"> <div class="flex-expand mb-1 mt-half">
<input type="number" min="0" id="inv-uses" class="input ~neutral !normal mr-1" value=1> <input type="number" min="0" id="create-uses" class="input ~neutral !normal mr-1" value=1>
<label for="inv-inf-uses" class="button ~neutral !normal"> <label for="create-inf-uses" class="button ~neutral !normal">
<span></span> <span></span>
<input type="checkbox" class="unfocused" id="inv-inf-uses" aria-label="Set uses to infinite"> <input type="checkbox" class="unfocused" id="create-inf-uses" aria-label="Set uses to infinite">
</label> </label>
</div> </div>
<p class="support unfocused"><span class="badge ~critical">Warning</span> invites with infinite uses can be used abusively.</p> <p class="support unfocused" id="create-inf-uses-warning"><span class="badge ~critical">Warning</span> invites with infinite uses can be used abusively.</p>
<label class="label supra">Profile</label> <label class="label supra">Profile</label>
<div class="select ~neutral !normal mb-1 mt-half" id="inv-profile"> <div class="select ~neutral !normal mb-1 mt-half">
<select> <select id="create-profile">
<option>Friends</option>
<option>Family</option>
<option>No Profile</option>
</select> </select>
</div> </div>
<label class="label supra">Send to</label> <label class="label supra">Send to</label>
<div class="flex-expand mb-1 mt-half"> <div class="flex-expand mb-1 mt-half">
<input type="email" id="inv-email" class="input ~neutral !normal mr-1" placeholder="example@example.com"> <input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
<label for="inv-email-enabled" class="button ~neutral !normal"> <label for="create-send-to-enabled" class="button ~neutral !normal">
<input type="checkbox" id="inv-email-enabled" aria-label="Send to address enabled"> <input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
</label> </label>
</div> </div>
<span class="button ~urge !normal supra full-width center lg">Create</span> <span class="button ~urge !normal supra full-width center lg" id="create-submit">Create</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,74 +1,24 @@
import { toggleTheme, loadTheme } from "./modules/theme.js"; import { toggleTheme, loadTheme } from "./modules/theme.js";
import { Modal } from "./modules/modal.js"; import { Modal } from "./modules/modal.js";
import { Tabs } from "./modules/tabs.js"; import { Tabs } from "./modules/tabs.js";
import { inviteList } from "./modules/invites.js"; import { inviteList, createInvite } from "./modules/invites.js";
import { notificationBox } from "./modules/common.js"; import { _post, notificationBox, whichAnimationEvent } from "./modules/common.js";
loadTheme(); loadTheme();
(document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme; (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.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); 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 loadAccounts = function () {
const rows: HTMLTableRowElement[] = Array.from(document.getElementById("accounts-list").children); 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 = {} as Modals;
window.modals.login = new Modal(document.getElementById('modal-login'), true); 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')); window.modals.addUser = new Modal(document.getElementById('modal-add-user'));
(document.getElementById('accounts-add-user') as HTMLSpanElement).onclick = window.modals.addUser.toggle; (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); 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) { function login(username: string, password: string) {
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.responseType = 'json'; req.responseType = 'json';
@ -234,8 +174,7 @@ function login(username: string, password: string) {
errorMsg = "Unknown error"; errorMsg = "Unknown error";
} }
if (!refresh) { if (!refresh) {
errorMessage(window.modals.login.modal.querySelector("aside"), errorMsg, 5); window.notifications.customError("loginError", errorMsg);
} else { } else {
window.modals.login.show(); window.modals.login.show();
} }
@ -245,6 +184,8 @@ function login(username: string, password: string) {
window.modals.login.close(); window.modals.login.close();
window.invites.reload(); window.invites.reload();
setInterval(window.invites.reload, 30*1000); setInterval(window.invites.reload, 30*1000);
document.getElementById("logout-button").classList.remove("unfocused");
/*generateInvites(); /*generateInvites();
setInterval((): void => generateInvites(), 60 * 1000); setInterval((): void => generateInvites(), 60 * 1000);
addOptions(30, document.getElementById('days') as HTMLSelectElement); 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 username = (document.getElementById("login-user") as HTMLInputElement).value;
const password = (document.getElementById("login-password") as HTMLInputElement).value; const password = (document.getElementById("login-password") as HTMLInputElement).value;
if (!username || !password) { 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; return;
} }
login(username, password, true); login(username, password);
}); });
login("", ""); 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;
}
});

View File

@ -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 addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => { export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => {
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("GET", window.URLBase + url, true); req.open("GET", window.URLBase + url, true);
req.responseType = 'json'; req.responseType = 'json';
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); 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)); 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("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); 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)); 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.open("DELETE", window.URLBase + url, true);
req.setRequestHeader("Authorization", "Bearer " + window.token); req.setRequestHeader("Authorization", "Bearer " + window.token);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); 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)); req.send(JSON.stringify(data));
} }
@ -100,7 +123,8 @@ export function toClipboard (str: string) {
export class notificationBox implements NotificationBox { export class notificationBox implements NotificationBox {
private _box: HTMLDivElement; private _box: HTMLDivElement;
timeout: number private _errorTypes: { [type: string]: boolean } = {};
timeout: number;
constructor(box: HTMLDivElement, timeout?: number) { this._box = box; this.timeout = timeout || 5; } constructor(box: HTMLDivElement, timeout?: number) { this._box = box; this.timeout = timeout || 5; }
private _error = (message: string): HTMLElement => { private _error = (message: string): HTMLElement => {
@ -115,14 +139,26 @@ export class notificationBox implements NotificationBox {
return noti; return noti;
} }
private _connectionError: boolean = false; connectionError = () => { this.customError("connectionError", "Couldn't connect to jfa-go"); }
connectionError = () => {
const noti = this._error("Couldn't connect to jfa-go."); customError = (type: string, message: string) => {
if (this._connectionError) { this._errorTypes[type] = this._errorTypes[type] || false;
this._box.querySelector("aside.notification-error").remove(); 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._box.appendChild(noti);
this._connectionError = true; this._errorTypes[type] = true;
setTimeout(() => { this._box.removeChild(noti); this._connectionError = false; }, this.timeout*1000); 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";
}

View File

@ -1,6 +1,9 @@
import { _get, _post, _delete, toClipboard } from "../modules/common.js"; import { _get, _post, _delete, toClipboard } from "../modules/common.js";
export class DOMInvite implements Invite { export class DOMInvite implements Invite {
// TODO
// add setProfile
//
updateNotify = (checkbox: HTMLInputElement) => { updateNotify = (checkbox: HTMLInputElement) => {
let state: { [code: string]: { [type: string]: boolean } } = {}; let state: { [code: string]: { [type: string]: boolean } } = {};
let revertChanges: () => void; 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)) { if (req.readyState == 4 && (req.status == 200 || req.status == 204)) {
this.remove(); this.remove();
const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { "detail": this.code });
document.dispatchEvent(inviteDeletedEvent);
} }
}); } })
private _code: string = "None"; private _code: string = "None";
get code(): string { return this._code; } get code(): string { return this._code; }
@ -153,7 +158,23 @@ export class DOMInvite implements Invite {
innerHTML += `<option value="${profile}" ${((profile == selected) && !noProfile) ? "selected" : ""}>${profile}</option>`; innerHTML += `<option value="${profile}" ${((profile == selected) && !noProfile) ? "selected" : ""}>${profile}</option>`;
} }
select.innerHTML = 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; private _container: HTMLDivElement;
@ -202,7 +223,21 @@ export class DOMInvite implements Invite {
<a class="code monospace mr-1" href=""></a> <a class="code monospace mr-1" href=""></a>
<span class="button ~info !normal" title="Copy invite link"><i class="ri-file-copy-line"></i></span> <span class="button ~info !normal" title="Copy invite link"><i class="ri-file-copy-line"></i></span>
`; `;
(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._infoArea = document.createElement('div') as HTMLDivElement;
this._header.appendChild(this._infoArea); this._header.appendChild(this._infoArea);
@ -252,6 +287,8 @@ export class DOMInvite implements Invite {
<span>On user creation</span> <span>On user creation</span>
</label> </label>
`; `;
(this._left.querySelector("select") as HTMLSelectElement).onchange = this.updateProfile;
const notifyExpiry = this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement; const notifyExpiry = this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement;
notifyExpiry.onchange = () => { this._notifyExpiry = notifyExpiry.checked; this.updateNotify(notifyExpiry); }; notifyExpiry.onchange = () => { this._notifyExpiry = notifyExpiry.checked; this.updateNotify(notifyExpiry); };
@ -276,6 +313,8 @@ export class DOMInvite implements Invite {
this.expanded = false; this.expanded = false;
this.update(invite); this.update(invite);
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
} }
update = (invite: Invite) => { update = (invite: Invite) => {
@ -295,19 +334,27 @@ export class DOMInvite implements Invite {
remove = () => { this._container.remove(); } 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 { export class inviteList implements inviteList {
private _list: HTMLDivElement; private _list: HTMLDivElement;
private _empty: boolean; 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 }; invites: { [code: string]: DOMInvite };
constructor() { constructor() {
this._list = document.getElementById('invites') as HTMLDivElement; this._list = document.getElementById('invites') as HTMLDivElement;
this.empty = true; this.empty = true;
this.invites = {}; 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; } get empty(): boolean { return this._empty; }
@ -340,14 +387,12 @@ export class inviteList implements inviteList {
this._list.appendChild(domInv.asElement()); this._list.appendChild(domInv.asElement());
} }
reload = () => { _get("/invites", null, (req: XMLHttpRequest) => { reload = () => _get("/invites", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
let data = req.response; let data = req.response;
window.availableProfiles = data["profiles"]; window.availableProfiles = data["profiles"];
for (let code in this.invites) { document.dispatchEvent(this._profileLoadEvent);
this.invites[code].loadProfiles(); if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) {
}
if (data["invites"] === undefined || data["invites"].length == 0) {
this.empty = true; this.empty = true;
return; return;
} }
@ -370,7 +415,7 @@ export class inviteList implements inviteList {
delete this.invites[code]; delete this.invites[code];
} }
} }
}) } })
} }
@ -395,65 +440,157 @@ function parseInvite(invite: { [f: string]: string | number | string[][] | boole
return parsed; 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 <span> 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 = () => {
function addInvite(invite: Invite): void { const fieldIDs = ["create-days", "create-hours", "create-minutes"];
const list = document.getElementById('invites') as HTMLDivElement; for (let i = 0; i < fieldIDs.length; i++) {
const container = document.createElement('div') as HTMLDivElement; const field = document.getElementById(fieldIDs[i]);
container.classList.add("inv"); field.textContent = '';
container.id = "invite-" + invite.code; for (let n = 0; n <= this._count; n++) {
// invite header const opt = document.createElement("option") as HTMLOptionElement;
const header = document.createElement("div") as HTMLDivElement; opt.textContent = ""+n;
(() => { opt.value = ""+n;
header.classList.add("card", "~neutral", "!normal", "inv-header", "flex-expand", "mt-half"); field.appendChild(opt);
// 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 = `
<a class="code monospace">None</a>
`;
return;
} }
const link = window.location.href.split("#")[0] + "invite/" + invite.code;
let innerHTML = `
<a class="code monospace mr-1" href="${link}">${invite.code.replace(/-/g, '-')}</a>
<span class="button ~info !normal" title="Copy invite link"><i class="ri-file-copy-line"></i></span>
`;
if (invite.email) {
let email = invite.email;
if (!invite.email.includes("Failed to send to")) {
email = "Sent to " + email;
}
innerHTML += `
<span class="support ml-1">${email}</span>
`;
}
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 = `
<span class="inv-expiry mr-1">${invite.expiresIn}</span>
<span class="button ~critical !normal inv-delete">Delete</span>
<label>
<i class="icon ri-arrow-down-s-line not-rotated"></i>
<input class="toggle-details
`; /// LINE 70
})();
})();
if (invite.empty) {
container.appendChild(header);
list.appendChild(container);
return;
} }
} }
*/ 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);
}
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 = `<option value="noProfile">No Profile</option>`;
for (let profile of window.availableProfiles) {
innerHTML += `<option value="${profile}">${profile}</option>`;
}
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);
}
});
}
constructor() {
this._populateNumbers();
this.days = 0;
this.hours = 0;
this.minutes = 30;
this._infUses.onchange = () => { this.infiniteUses = this.infiniteUses; };
this.infiniteUses = false;
this._sendToEnabled.onchange = () => { this.sendToEnabled = this.sendToEnabled; };
this.sendToEnabled = false;
this._createButton.onclick = this.create;
this.sendTo = "";
this.uses = 1;
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
}
}

View File

@ -14,7 +14,7 @@ declare interface Window {
URLBase: string; URLBase: string;
modals: Modals; modals: Modals;
cssFile: string; cssFile: string;
availableProfiles: Array<any>; availableProfiles: string[];
jfUsers: Array<Object>; jfUsers: Array<Object>;
notifications_enabled: boolean; notifications_enabled: boolean;
token: string; token: string;
@ -27,7 +27,8 @@ declare interface Window {
} }
declare interface NotificationBox { declare interface NotificationBox {
connectionError: (timeout: number) => void; connectionError: () => void;
customError: (type: string, message: string) => void;
} }
declare interface Tabs { declare interface Tabs {