add login and logout commands in frontend

Signed-off-by: Tobias Erbshäußer <tobias@tesoft.dev>
This commit is contained in:
2026-05-24 09:22:37 +02:00
parent c7e59563b6
commit b49126ded4
4 changed files with 131 additions and 3 deletions
+4
View File
@@ -26,6 +26,10 @@ export class TesoftInput extends TesoftComponent {
this.input.focus(); this.input.focus();
} }
get value(): string {
return this.input.value;
}
set value(value: string) { set value(value: string) {
this.input.value = value; this.input.value = value;
} }
+1
View File
@@ -78,6 +78,7 @@
} }
dialog.search > div { dialog.search > div {
color: var(--light-gray);
display: grid; display: grid;
gap: var(--medium-padding); gap: var(--medium-padding);
grid-template-columns: 1fr; grid-template-columns: 1fr;
+84 -3
View File
@@ -1,4 +1,4 @@
import {TesoftComponent} from "../scripts/main.ts"; import {Debouncer, sendApiPost, TesoftComponent} from "../scripts/main.ts";
import {TesoftButton} from "./button.ts"; import {TesoftButton} from "./button.ts";
import {TesoftInput} from "./input.ts"; import {TesoftInput} from "./input.ts";
@@ -10,6 +10,9 @@ export class TesoftNav extends TesoftComponent {
private readonly searchButton: TesoftButton; private readonly searchButton: TesoftButton;
private readonly searchDialog: HTMLDialogElement; private readonly searchDialog: HTMLDialogElement;
private readonly searchInput: TesoftInput; private readonly searchInput: TesoftInput;
private readonly searchResultsDiv: HTMLDivElement;
private readonly debouncedSearch: Debouncer<[string, boolean]>;
private readonly commands: Map<string, (text: string) => Promise<void>>;
constructor() { constructor() {
super(); super();
@@ -27,6 +30,12 @@ export class TesoftNav extends TesoftComponent {
this.searchButton = shadowRoot.querySelector<TesoftButton>("tesoft-button.search")!; this.searchButton = shadowRoot.querySelector<TesoftButton>("tesoft-button.search")!;
this.searchDialog = shadowRoot.querySelector<HTMLDialogElement>("dialog.search")!; this.searchDialog = shadowRoot.querySelector<HTMLDialogElement>("dialog.search")!;
this.searchInput = shadowRoot.querySelector<TesoftInput>("dialog.search tesoft-input")!; 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.smallMenuButton.addEventListener("click", () => {
this.updateSmallMenu(); this.updateSmallMenu();
@@ -36,14 +45,21 @@ export class TesoftNav extends TesoftComponent {
this.smallMenuDialog.style.width = `${this.smallMenuButton.offsetWidth}px`; 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.searchDialog.showModal();
this.searchInput.value = ""; this.searchInput.value = "";
this.searchInput.focus(); this.searchInput.focus();
}); });
this.searchInput.addEventListener("input", () => { 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", () => { 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); customElements.define("tesoft-nav", TesoftNav);
+42
View File
@@ -83,3 +83,45 @@ export function onReady(callback: () => void, ...components: TesoftComponent[])
domLoaded(); 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),
});
}