Frontend basics

This commit is contained in:
Piotr Dec 2025-10-16 23:07:06 +02:00
parent 8565ce19fe
commit e310930d9e
Signed by: stawros
GPG key ID: 74B18A3F0F1E99C0
10 changed files with 607 additions and 58 deletions

66
app/static/css/style.css Normal file
View file

@ -0,0 +1,66 @@
html, body {
height: 100%;
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial;
}
.layout {
display: grid;
grid-template-columns: 320px 1fr;
height: 100vh;
}
.left {
border-right: 1px solid #ddd;
padding: 12px;
overflow: auto;
}
.right {
padding: 12px;
overflow: auto;
}
ul {
list-style: none;
padding-left: 16px;
}
.node {
cursor: pointer;
}
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.row {
margin-bottom: 8px;
display: flex;
gap: 8px;
align-items: center;
}
input[type="text"], input[type="url"], input[type="password"], textarea, select {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 6px;
}
.section {
border: 1px solid #eee;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
}
.danger {
color: #b10000
}

187
app/static/js/app.js Normal file
View 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));