favourited notes

This commit is contained in:
Mateusz Gruszczyński
2026-09-07 16:39:58 +02:00
parent 87b61d6088
commit f9cf17554a
25 changed files with 1534 additions and 22 deletions
+39 -3
View File
@@ -109,6 +109,7 @@ const registerLink = document.querySelector("#footer-register");
const registrationEnabled = document.body.dataset.registrationEnabled === "true";
const resourcesDialog = document.querySelector("#resources-dialog");
const resourcesList = document.querySelector("#resources-list");
const favoritesList = document.querySelector("#favorites-list");
const resourcesError = document.querySelector("#resources-error");
const resourcesSearch = document.querySelector("#resources-search");
const resourcesPerPage = document.querySelector("#resources-per-page");
@@ -174,11 +175,46 @@ document.addEventListener("click", event => {
document.addEventListener("keydown", event => {
if (event.key === "Escape") closeResourcePasswordMenus();
});
function renderFavorites(items) {
if (!favoritesList) return;
favoritesList.innerHTML = items.length ? "" : `<p>${escapeHtml(t("favorites.empty", {}, "No favorites yet."))}</p>`;
for (const item of items) {
const row = document.createElement("article");
row.className = "resource-row resource-row--favorite";
const workspace = item.workspace_title
? ` · ${t("common.workspace", {}, "Workspace")}: ${item.workspace_title}`
: "";
row.innerHTML = `<div class="resource-copy"><div class="resource-title-line"><a href="${escapeHtml(safeAppUrl(item.url))}" title="${escapeHtml(item.title)}">${escapeHtml(item.title)}</a></div><small>${escapeHtml(t("common.note", {}, "Note"))}${escapeHtml(workspace)}</small></div><button class="resource-favorite-remove" type="button" data-unfavorite title="${escapeHtml(t("favorites.remove", {}, "Remove from favorites"))}" aria-label="${escapeHtml(t("favorites.remove", {}, "Remove from favorites"))}">★</button>`;
row.querySelector("[data-unfavorite]")?.addEventListener("click", async event => {
const button = event.currentTarget;
button.disabled = true;
try {
const target = { kind: item.kind, slug: item.slug };
if (item.workspace_slug) target.workspace_slug = item.workspace_slug;
await api("/api/auth/favorites", { method: "DELETE", headers: authHeaders(), body: JSON.stringify(target) });
toast.success(t("favorites.removed", {}, "Removed from favorites."), { title: t("favorites.title", {}, "Favorites") });
await loadResources();
} catch (error) {
button.disabled = false;
resourcesError.textContent = error.message;
toast.danger(error.message, { title: t("favorites.updateFailed", {}, "Could not update favorites") });
}
});
favoritesList.append(row);
}
}
async function loadResources() {
resourcesError.textContent = ""; resourcesList.innerHTML = "<p>Loading…</p>";
resourcesError.textContent = ""; resourcesList.innerHTML = "<p>Loading…</p>"; if (favoritesList) favoritesList.innerHTML = "<p>Loading…</p>";
try {
const params = new URLSearchParams({ q: resourcesSearch.value.trim(), page: String(resourcesPage), per_page: resourcesPerPage.value });
const data = await api(`/api/auth/resources?${params}`, { headers: authHeaders() });
const favoriteParams = new URLSearchParams({ q: resourcesSearch.value.trim() });
const [data, favorites] = await Promise.all([
api(`/api/auth/resources?${params}`, { headers: authHeaders() }),
api(`/api/auth/favorites?${favoriteParams}`, { headers: authHeaders() }),
]);
renderFavorites(favorites.items || []);
resourcesPage = data.pagination.page;
const items = data.items.map(item => ({ ...item, url: item.kind === "workspace" ? `/w/${item.slug}` : `/p/${item.slug}` }));
resourcesList.innerHTML = items.length ? "" : "<p>No assigned items yet.</p>";
@@ -395,7 +431,7 @@ async function loadResources() {
resourcesList.append(row);
}
renderResourcesPagination(data.pagination);
} catch (e) { resourcesList.innerHTML = ""; resourcesPagination.innerHTML = ""; resourcesError.textContent = e.message; toast.danger(e.message, { title: "Could not load your items" }); }
} catch (e) { resourcesList.innerHTML = ""; if (favoritesList) favoritesList.innerHTML = ""; resourcesPagination.innerHTML = ""; resourcesError.textContent = e.message; toast.danger(e.message, { title: "Could not load your items" }); }
}
resourcesSearch?.addEventListener("input", () => { clearTimeout(resourcesSearchTimer); resourcesSearchTimer = setTimeout(() => { resourcesPage = 1; loadResources(); }, 250); });