174 lines
6.5 KiB
HTML
174 lines
6.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Notes</title>
|
|
<link rel="stylesheet" href="main.css">
|
|
<script src="./cookies.js"></script>
|
|
<script src="./jwt.js"></script>
|
|
</head>
|
|
<body>
|
|
<h1>KeepNote</h1>
|
|
<div id="toolbar">
|
|
<button id="logout">Log out</button>
|
|
</div>
|
|
<form id="create-note">
|
|
<input name="title" type="text" placeholder="title" required />
|
|
<input name="content" type="text" placeholder="content" />
|
|
<button>Add note</button>
|
|
</form>
|
|
<div id="error"></div>
|
|
<ul id="notes"></ul>
|
|
<script>
|
|
const API = "http://127.0.0.1:8081";
|
|
const errorBox = document.getElementById("error");
|
|
let editingId = null;
|
|
|
|
function token() {
|
|
return cookieEater("accessToken");
|
|
}
|
|
|
|
function showError(msg) {
|
|
errorBox.textContent = msg;
|
|
}
|
|
|
|
async function api(path, options) {
|
|
options = options || {};
|
|
options.headers = Object.assign({ "X-Access-Token": token() }, options.headers || {});
|
|
const res = await fetch(`${API}${path}`, options);
|
|
if (res.status === 401) {
|
|
window.location.replace("./auth.html");
|
|
throw new Error("unauthorized");
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function renderNote(note) {
|
|
const li = document.createElement("li");
|
|
if (editingId === note.id) {
|
|
const form = document.createElement("form");
|
|
form.className = "edit-note";
|
|
const title = document.createElement("input");
|
|
title.name = "title";
|
|
title.value = note.title || "";
|
|
const content = document.createElement("input");
|
|
content.name = "content";
|
|
content.value = note.content || "";
|
|
const tags = document.createElement("input");
|
|
tags.name = "tags";
|
|
tags.value = (note.tags || []).join(", ");
|
|
tags.placeholder = "tags (comma separated)";
|
|
const save = document.createElement("button");
|
|
save.textContent = "Save";
|
|
const cancel = document.createElement("button");
|
|
cancel.type = "button";
|
|
cancel.textContent = "Cancel";
|
|
cancel.addEventListener("click", () => { editingId = null; loadNotes(); });
|
|
form.append(title, content, tags, save, cancel);
|
|
form.addEventListener("submit", (event) => updateNote(event, note.id, title, content, tags));
|
|
li.appendChild(form);
|
|
return li;
|
|
}
|
|
|
|
const title = document.createElement("strong");
|
|
title.textContent = note.title || "(untitled)";
|
|
li.appendChild(title);
|
|
if (note.content) {
|
|
li.appendChild(document.createTextNode(" - " + note.content));
|
|
}
|
|
for (const tag of note.tags || []) {
|
|
const badge = document.createElement("span");
|
|
badge.className = "tag";
|
|
badge.textContent = tag;
|
|
li.appendChild(badge);
|
|
}
|
|
const edit = document.createElement("button");
|
|
edit.textContent = "Edit";
|
|
edit.addEventListener("click", () => { editingId = note.id; loadNotes(); });
|
|
li.appendChild(edit);
|
|
const del = document.createElement("button");
|
|
del.textContent = "Delete";
|
|
del.addEventListener("click", () => deleteNote(note.id));
|
|
li.appendChild(del);
|
|
return li;
|
|
}
|
|
|
|
async function loadNotes() {
|
|
const res = await api("/action/getNotes");
|
|
if (!res.ok) {
|
|
showError(`failed to load notes: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
const notes = await res.json();
|
|
const list = document.getElementById("notes");
|
|
list.innerHTML = "";
|
|
for (const note of notes) {
|
|
list.appendChild(renderNote(note));
|
|
}
|
|
}
|
|
|
|
async function createNote(event) {
|
|
event.preventDefault();
|
|
showError("");
|
|
const data = Object.fromEntries(new FormData(event.target).entries());
|
|
const res = await api("/action/writeNote", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ payload: data })
|
|
});
|
|
if (!res.ok) {
|
|
showError(`failed to add note: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
event.target.reset();
|
|
await loadNotes();
|
|
}
|
|
|
|
async function updateNote(event, id, titleInput, contentInput, tagsInput) {
|
|
event.preventDefault();
|
|
showError("");
|
|
const tags = tagsInput.value.split(",").map((t) => t.trim()).filter((t) => t !== "");
|
|
const res = await api("/action/updateNote", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ payload: { id: id, title: titleInput.value, content: contentInput.value, tags: tags } })
|
|
});
|
|
if (!res.ok) {
|
|
showError(`failed to update note: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
editingId = null;
|
|
await loadNotes();
|
|
}
|
|
|
|
async function deleteNote(id) {
|
|
showError("");
|
|
const res = await api("/action/deleteNote", {
|
|
method: "DELETE",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ payload: { id: id } })
|
|
});
|
|
if (!res.ok) {
|
|
showError(`failed to delete note: ${res.status} ${await res.text()}`);
|
|
return;
|
|
}
|
|
await loadNotes();
|
|
}
|
|
|
|
async function logout() {
|
|
try {
|
|
await api("/auth/logout");
|
|
} catch (e) { /* ignore */ }
|
|
document.cookie = "accessToken=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/";
|
|
window.location.replace("./auth.html");
|
|
}
|
|
|
|
document.getElementById("create-note").addEventListener("submit", createNote);
|
|
document.getElementById("logout").addEventListener("click", logout);
|
|
|
|
loadNotes();
|
|
</script>
|
|
</body>
|
|
</html>
|