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();
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user