Frontend basics
This commit is contained in:
parent
8565ce19fe
commit
e310930d9e
10 changed files with 607 additions and 58 deletions
187
app/static/js/app.js
Normal file
187
app/static/js/app.js
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
let selectedNode = { type: "group", path: "/" }; // type: "group" | "entry"
|
||||
let currentKind = "simple";
|
||||
|
||||
const treeDiv = document.getElementById("tree");
|
||||
const currentPathEl = document.getElementById("current-path");
|
||||
|
||||
const editorSimple = document.getElementById("editor-simple");
|
||||
const editorComplex = document.getElementById("editor-complex");
|
||||
const kindSelect = document.getElementById("entry-kind");
|
||||
|
||||
function showEditor(kind) {
|
||||
editorSimple.style.display = (kind === "simple") ? "block" : "none";
|
||||
editorComplex.style.display = (kind === "complex") ? "block" : "none";
|
||||
}
|
||||
|
||||
kindSelect.addEventListener("change", () => {
|
||||
currentKind = kindSelect.value;
|
||||
showEditor(currentKind);
|
||||
});
|
||||
|
||||
async function api(path, opts = {}) {
|
||||
const resp = await fetch(path, opts);
|
||||
if (!resp.ok) {
|
||||
const txt = await resp.text();
|
||||
throw new Error("API error: " + txt);
|
||||
}
|
||||
if (resp.headers.get("Content-Type")?.includes("application/json")) {
|
||||
return resp.json();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderTree(node) {
|
||||
const ul = document.createElement("ul");
|
||||
|
||||
const mkEntryLi = (e) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = "🔑 " + e.title;
|
||||
li.className = "node";
|
||||
li.onclick = () => {
|
||||
selectedNode = { type: "entry", path: e.path, kind: e.kind };
|
||||
currentPathEl.textContent = e.path;
|
||||
kindSelect.value = e.kind;
|
||||
showEditor(e.kind);
|
||||
if (e.kind === "simple") {
|
||||
document.getElementById("s-key").value = e.title;
|
||||
document.getElementById("s-value").value = "";
|
||||
} else {
|
||||
document.getElementById("c-title").value = e.title;
|
||||
document.getElementById("c-username").value = "";
|
||||
document.getElementById("c-password").value = "";
|
||||
document.getElementById("c-url").value = "";
|
||||
document.getElementById("c-notes").value = "";
|
||||
}
|
||||
};
|
||||
return li;
|
||||
};
|
||||
|
||||
const mkGroupLi = (g) => {
|
||||
const li = document.createElement("li");
|
||||
const header = document.createElement("div");
|
||||
header.textContent = "📂 " + (g.name || "/");
|
||||
header.className = "node";
|
||||
header.onclick = () => {
|
||||
selectedNode = { type: "group", path: g.path };
|
||||
currentPathEl.textContent = g.path;
|
||||
};
|
||||
li.appendChild(header);
|
||||
|
||||
const inner = renderTreeChildren(g);
|
||||
li.appendChild(inner);
|
||||
return li;
|
||||
};
|
||||
|
||||
function renderTreeChildren(node) {
|
||||
const wrap = document.createElement("ul");
|
||||
node.groups.forEach(sg => wrap.appendChild(mkGroupLi(sg)));
|
||||
node.entries.forEach(e => wrap.appendChild(mkEntryLi(e)));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
ul.appendChild(mkGroupLi(node));
|
||||
treeDiv.innerHTML = "";
|
||||
treeDiv.appendChild(ul);
|
||||
}
|
||||
|
||||
async function refreshTree() {
|
||||
const data = await api("/api/v1/tree");
|
||||
renderTree(data);
|
||||
}
|
||||
|
||||
// Toolbar lewej kolumny
|
||||
document.getElementById("btn-refresh").onclick = refreshTree;
|
||||
|
||||
document.getElementById("btn-add-group").onclick = async () => {
|
||||
const name = prompt("Nazwa nowej grupy:");
|
||||
if (!name) return;
|
||||
await api("/api/v1/group", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ name, parent_path: selectedNode.type === "group" ? selectedNode.path : "/" })
|
||||
});
|
||||
await refreshTree();
|
||||
};
|
||||
|
||||
document.getElementById("btn-add-entry").onclick = async () => {
|
||||
const parent_path = selectedNode.type === "group" ? selectedNode.path : "/";
|
||||
const kind = kindSelect.value;
|
||||
if (kind === "simple") {
|
||||
const key = prompt("Klucz (title):"); if (!key) return;
|
||||
const value = prompt("Wartość (password):"); if (value === null) return;
|
||||
await api("/api/v1/entry", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ kind, parent_path, data: { key, value } })
|
||||
});
|
||||
} else {
|
||||
const title = prompt("Tytuł:"); if (!title) return;
|
||||
await api("/api/v1/entry", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ kind, parent_path, data: { title, username:"", password:"", url:"", notes:"" } })
|
||||
});
|
||||
}
|
||||
await refreshTree();
|
||||
};
|
||||
|
||||
// Edytory
|
||||
document.getElementById("s-save").onclick = async () => {
|
||||
if (selectedNode.type !== "entry") { alert("Wybierz wpis"); return; }
|
||||
const data = { key: document.getElementById("s-key").value, value: document.getElementById("s-value").value };
|
||||
await api("/api/v1/entry", {
|
||||
method: "PATCH",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ path: selectedNode.path, kind: "simple", data })
|
||||
});
|
||||
await refreshTree();
|
||||
};
|
||||
document.getElementById("s-delete").onclick = async () => {
|
||||
if (selectedNode.type !== "entry") return;
|
||||
if (!confirm("Usunąć wpis?")) return;
|
||||
await api("/api/v1/entry?path=" + encodeURIComponent(selectedNode.path), { method: "DELETE" });
|
||||
await refreshTree();
|
||||
};
|
||||
|
||||
document.getElementById("c-save").onclick = async () => {
|
||||
if (selectedNode.type !== "entry") { alert("Wybierz wpis"); return; }
|
||||
const data = {
|
||||
title: document.getElementById("c-title").value,
|
||||
username: document.getElementById("c-username").value,
|
||||
password: document.getElementById("c-password").value,
|
||||
url: document.getElementById("c-url").value,
|
||||
notes: document.getElementById("c-notes").value
|
||||
};
|
||||
await api("/api/v1/entry", {
|
||||
method: "PATCH",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ path: selectedNode.path, kind: "complex", data })
|
||||
});
|
||||
await refreshTree();
|
||||
};
|
||||
document.getElementById("c-delete").onclick = async () => {
|
||||
if (selectedNode.type !== "entry") return;
|
||||
if (!confirm("Usunąć wpis?")) return;
|
||||
await api("/api/v1/entry?path=" + encodeURIComponent(selectedNode.path), { method: "DELETE" });
|
||||
await refreshTree();
|
||||
};
|
||||
|
||||
document.getElementById("btn-move-entry").onclick = async () => {
|
||||
if (selectedNode.type !== "entry") { alert("Wybierz wpis"); return; }
|
||||
const target = document.getElementById("target-group").value || "/";
|
||||
await api("/api/v1/entry/move", {
|
||||
method: "PATCH",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({ path: selectedNode.path, target_group_path: target })
|
||||
});
|
||||
await refreshTree();
|
||||
};
|
||||
|
||||
document.getElementById("btn-save-all").onclick = async () => {
|
||||
await api("/api/v1/save", { method: "POST" });
|
||||
alert("Zapisano do bazy");
|
||||
};
|
||||
|
||||
// Start
|
||||
showEditor(currentKind);
|
||||
refreshTree().catch(err => console.error(err));
|
||||
Loading…
Add table
Add a link
Reference in a new issue