add login and logout commands in frontend
Signed-off-by: Tobias Erbshäußer <tobias@tesoft.dev>
This commit is contained in:
@@ -26,6 +26,10 @@ export class TesoftInput extends TesoftComponent {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this.input.value;
|
||||
}
|
||||
|
||||
set value(value: string) {
|
||||
this.input.value = value;
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
}
|
||||
|
||||
dialog.search > div {
|
||||
color: var(--light-gray);
|
||||
display: grid;
|
||||
gap: var(--medium-padding);
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {TesoftComponent} from "../scripts/main.ts";
|
||||
import {Debouncer, sendApiPost, TesoftComponent} from "../scripts/main.ts";
|
||||
import {TesoftButton} from "./button.ts";
|
||||
import {TesoftInput} from "./input.ts";
|
||||
|
||||
@@ -10,6 +10,9 @@ export class TesoftNav extends TesoftComponent {
|
||||
private readonly searchButton: TesoftButton;
|
||||
private readonly searchDialog: HTMLDialogElement;
|
||||
private readonly searchInput: TesoftInput;
|
||||
private readonly searchResultsDiv: HTMLDivElement;
|
||||
private readonly debouncedSearch: Debouncer<[string, boolean]>;
|
||||
private readonly commands: Map<string, (text: string) => Promise<void>>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -27,6 +30,12 @@ export class TesoftNav extends TesoftComponent {
|
||||
this.searchButton = shadowRoot.querySelector<TesoftButton>("tesoft-button.search")!;
|
||||
this.searchDialog = shadowRoot.querySelector<HTMLDialogElement>("dialog.search")!;
|
||||
this.searchInput = shadowRoot.querySelector<TesoftInput>("dialog.search tesoft-input")!;
|
||||
this.searchResultsDiv = shadowRoot.querySelector<HTMLDivElement>("dialog.search .results")!;
|
||||
this.debouncedSearch = new Debouncer(this.applySearch.bind(this));
|
||||
this.commands = new Map<string, (text: string) => Promise<void>>([
|
||||
["login", this.login.bind(this)],
|
||||
["logout", this.logout.bind(this)],
|
||||
]);
|
||||
|
||||
this.smallMenuButton.addEventListener("click", () => {
|
||||
this.updateSmallMenu();
|
||||
@@ -36,14 +45,21 @@ export class TesoftNav extends TesoftComponent {
|
||||
this.smallMenuDialog.style.width = `${this.smallMenuButton.offsetWidth}px`;
|
||||
});
|
||||
|
||||
this.searchButton.addEventListener("click", () => {
|
||||
this.searchButton.addEventListener("click", async () => {
|
||||
await this.applySearch("", false);
|
||||
this.searchDialog.showModal();
|
||||
this.searchInput.value = "";
|
||||
this.searchInput.focus();
|
||||
});
|
||||
|
||||
this.searchInput.addEventListener("input", () => {
|
||||
// TODO
|
||||
this.debouncedSearch.run(this.searchInput.value, false);
|
||||
});
|
||||
|
||||
this.searchInput.addEventListener("keyup", async (event) => {
|
||||
if (event.key === "Enter") {
|
||||
await this.applySearch(this.searchInput.value, true);
|
||||
}
|
||||
});
|
||||
|
||||
shadowRoot.addEventListener("slotchange", () => {
|
||||
@@ -68,6 +84,71 @@ export class TesoftNav extends TesoftComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async applySearch(text: string, confirmed: boolean) {
|
||||
if (confirmed) {
|
||||
this.debouncedSearch.stop();
|
||||
}
|
||||
|
||||
this.setResultText("");
|
||||
|
||||
if (text.startsWith(">")) {
|
||||
if (confirmed) {
|
||||
await this.applyCommand(text.substring(1).trimStart());
|
||||
} else {
|
||||
this.setResultText("Press enter to send command.");
|
||||
}
|
||||
} else {
|
||||
console.log(text);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
private async applyCommand(text: string) {
|
||||
const spaceIndex = text.indexOf(" ");
|
||||
const prefix = spaceIndex === -1 ? text : text.substring(0, spaceIndex);
|
||||
|
||||
const command = this.commands.get(prefix);
|
||||
if (command === undefined) {
|
||||
this.setResultText("Unknown command.");
|
||||
return;
|
||||
}
|
||||
await command(text.substring(prefix.length).trimStart());
|
||||
}
|
||||
|
||||
private async login(text: string) {
|
||||
const response = await sendApiPost("login", {
|
||||
password: text,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.setResultText("Login successful.");
|
||||
} else {
|
||||
const body = await response.json();
|
||||
this.setResultText(`Login failed: ${body.message ?? body}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async logout(_text: string) {
|
||||
const response = await sendApiPost("logout", {});
|
||||
|
||||
if (response.ok) {
|
||||
this.setResultText("Logout successful.");
|
||||
} else {
|
||||
const body = await response.json();
|
||||
this.setResultText(`Logout failed: ${body.message ?? body}`);
|
||||
}
|
||||
}
|
||||
|
||||
private setResultText(text: string) {
|
||||
this.searchResultsDiv.innerHTML = "";
|
||||
|
||||
if (text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
this.searchResultsDiv.appendChild(div);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("tesoft-nav", TesoftNav);
|
||||
|
||||
@@ -83,3 +83,45 @@ export function onReady(callback: () => void, ...components: TesoftComponent[])
|
||||
domLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
export class Debouncer<T extends any[]> {
|
||||
private timer: number | null;
|
||||
private readonly apply: (...args: T) => void;
|
||||
|
||||
constructor(apply: (...args: T) => void, timeoutMs: number = 300) {
|
||||
this.timer = null;
|
||||
this.apply = (...args: T) => {
|
||||
if (this.timer !== null) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
apply(...args);
|
||||
}, timeoutMs);
|
||||
};
|
||||
}
|
||||
|
||||
run(...args: T) {
|
||||
this.apply(...args);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.timer !== null) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sendApiPost(path: string, data: Record<string, any>): Promise<Response> {
|
||||
const url = new URL(`/api/${path}`, location.origin);
|
||||
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user