149 lines
3.7 KiB
JavaScript
Executable File
149 lines
3.7 KiB
JavaScript
Executable File
(function () {
|
|
const wall = document.getElementById("notes-wall");
|
|
const form = document.getElementById("notes-form");
|
|
const authorInput = document.getElementById("note-author");
|
|
const contentInput = document.getElementById("note-content");
|
|
const authorFilter = document.getElementById("notes-author-filter");
|
|
let notesCache = [];
|
|
|
|
if (!wall) return;
|
|
|
|
function escapeHtml(str) {
|
|
return str
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function renderNote(note) {
|
|
const el = document.createElement("div");
|
|
el.className = "note";
|
|
|
|
el.innerHTML = `
|
|
<div class="note-meta">
|
|
<span class="note-author">${escapeHtml(note.author_name)}</span>
|
|
<time datetime="${note.created_at}">
|
|
${new Date(note.created_at).toLocaleString()}
|
|
</time>
|
|
</div>
|
|
<div class="note-content">
|
|
${escapeHtml(note.content).replace(/\n/g, "<br>")}
|
|
</div>
|
|
`;
|
|
|
|
return el;
|
|
}
|
|
|
|
function normaliseAuthor(author) {
|
|
return (author || "").trim();
|
|
}
|
|
|
|
function populateAuthorFilter(notes) {
|
|
if (!authorFilter) return;
|
|
|
|
const previousValue = authorFilter.value;
|
|
const authors = Array.from(
|
|
new Set(notes.map(note => normaliseAuthor(note.author_name)).filter(Boolean))
|
|
).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
|
|
|
|
authorFilter.innerHTML = '<option value="">All authors</option>';
|
|
|
|
authors.forEach(author => {
|
|
const option = document.createElement("option");
|
|
option.value = author;
|
|
option.textContent = author;
|
|
authorFilter.appendChild(option);
|
|
});
|
|
|
|
authorFilter.value = authors.includes(previousValue) ? previousValue : "";
|
|
}
|
|
|
|
function renderNotes() {
|
|
wall.innerHTML = "";
|
|
|
|
const selectedAuthor = authorFilter ? authorFilter.value : "";
|
|
const notes = selectedAuthor
|
|
? notesCache.filter(note => normaliseAuthor(note.author_name) === selectedAuthor)
|
|
: notesCache;
|
|
|
|
if (notesCache.length === 0) {
|
|
wall.innerHTML = "<p>No notes yet.</p>";
|
|
return;
|
|
}
|
|
|
|
if (notes.length === 0) {
|
|
wall.innerHTML = "<p>No notes for this author.</p>";
|
|
return;
|
|
}
|
|
|
|
notes.forEach(note => {
|
|
wall.appendChild(renderNote(note));
|
|
});
|
|
}
|
|
|
|
async function loadNotes() {
|
|
wall.innerHTML = "<p>Loading notes…</p>";
|
|
|
|
try {
|
|
const res = await fetch("/api/notes");
|
|
if (!res.ok) throw new Error("Failed to fetch notes");
|
|
|
|
const notes = await res.json();
|
|
notesCache = notes;
|
|
populateAuthorFilter(notesCache);
|
|
renderNotes();
|
|
} catch (err) {
|
|
console.error(err);
|
|
wall.innerHTML = "<p>Could not load notes.</p>";
|
|
}
|
|
}
|
|
|
|
async function submitNote(e) {
|
|
e.preventDefault();
|
|
|
|
const author = authorInput.value.trim();
|
|
const content = contentInput.value.trim();
|
|
|
|
if (!author || !content) return;
|
|
|
|
form.querySelector("button").disabled = true;
|
|
|
|
try {
|
|
const res = await fetch("/api/notes", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
author_name: author,
|
|
content: content,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.detail || "Failed to post note");
|
|
}
|
|
|
|
contentInput.value = "";
|
|
await loadNotes();
|
|
} catch (err) {
|
|
alert(err.message);
|
|
} finally {
|
|
form.querySelector("button").disabled = false;
|
|
}
|
|
}
|
|
|
|
if (form) {
|
|
form.addEventListener("submit", submitNote);
|
|
}
|
|
|
|
if (authorFilter) {
|
|
authorFilter.addEventListener("change", renderNotes);
|
|
}
|
|
|
|
loadNotes();
|
|
})();
|