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
+74 -5
View File
@@ -12,12 +12,12 @@ installGlobalDiagnostics();
import { api } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { getNickname, getAccessToken, getAuthToken, getGuestId, setAccessToken } from "@rustpad/session";
import { getNickname, getAccessToken, getGuestId, setAccessToken } from "@rustpad/session";
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
import { askConfirm } from "@rustpad/modal";
import { isResourceAccessError, safeAppUrl } from "@rustpad/security";
import { consumeQueuedToast, toast } from "@rustpad/toast";
import { formatDateTime, formatNumber } from "@rustpad/i18n";
import { formatDateTime, formatNumber, t } from "@rustpad/i18n";
consumeQueuedToast();
@@ -27,6 +27,7 @@ let info;
const shareToken = new URLSearchParams(location.search).get("share");
let accessToken = shareToken || getAccessToken("workspace", slug);
let nickname = getNickname();
let accountSession = null;
getGuestId();
const workspaceWatchClientId = `workspace_watch_${crypto.randomUUID()}`;
let workspaceWatchSocket;
@@ -177,6 +178,57 @@ function deleteButton(note, inline = false) {
: "Read-write access is required to delete this note";
return `<button class="${classes}" data-delete-note="${escapeHtml(note.slug)}" data-note-title="${escapeHtml(note.title)}" ${disabled ? "disabled" : ""} title="${escapeHtml(reason)}">Delete</button>`;
}
function favoriteButton(note, inline = false) {
if (!accountSession) return "";
const active = Boolean(note.favorite);
const label = active
? t("favorites.remove", {}, "Remove from favorites")
: t("favorites.add", {}, "Add to favorites");
return `<button class="note-favorite-button${inline ? " note-favorite-button--inline" : ""}" type="button" data-favorite-note="${escapeHtml(note.slug)}" aria-pressed="${active}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}"><span aria-hidden="true">${active ? "★" : "☆"}</span></button>`;
}
function updateFavoriteButton(button, active) {
const favorite = Boolean(active);
const label = favorite
? t("favorites.remove", {}, "Remove from favorites")
: t("favorites.add", {}, "Add to favorites");
button.setAttribute("aria-pressed", String(favorite));
button.title = label;
button.setAttribute("aria-label", label);
const icon = button.querySelector("span");
if (icon) icon.textContent = favorite ? "★" : "☆";
}
async function toggleNoteFavorite(button) {
if (!accountSession) return;
const note = notesCache.find(item => item.slug === button.dataset.favoriteNote);
if (!note) return;
const current = Boolean(note.favorite);
button.disabled = true;
try {
const result = await api("/api/auth/favorites", {
method: current ? "DELETE" : "PUT",
body: JSON.stringify({ kind: "note", slug: note.slug, workspace_slug: slug }),
});
note.favorite = Boolean(result.favorite);
document.querySelectorAll(`[data-favorite-note="${CSS.escape(note.slug)}"]`).forEach(target => updateFavoriteButton(target, note.favorite));
toast.success(
note.favorite
? t("favorites.added", {}, "Added to favorites.")
: t("favorites.removed", {}, "Removed from favorites."),
{ title: t("favorites.title", {}, "Favorites") },
);
} catch (error) {
if (error.status === 401 || error.status === 403) {
const session = await validateCurrentSession();
if (!session) {
accountSession = null;
renderNotes();
}
}
toast.danger(error.message, { title: t("favorites.updateFailed", {}, "Could not update favorites") });
} finally {
if (button.isConnected) button.disabled = false;
}
}
function renderNotes(notes = notesCache) {
notesCache = notes;
setNotesView(notesView);
@@ -185,8 +237,10 @@ function renderNotes(notes = notesCache) {
return;
}
if (notesView === "table") {
notesList.innerHTML = `<div class="notes-table-scroll"><table><thead><tr><th>Name</th><th>Created by</th><th>Participants</th><th>Files</th><th>Revisions</th><th>Status</th><th>Updated</th><th class="notes-table-actions">Actions</th></tr></thead><tbody>${notes.map(note => `
const favoriteHeader = accountSession ? '<th class="notes-table-favorite"><span class="sr-only">Favorites</span>★</th>' : "";
notesList.innerHTML = `<div class="notes-table-scroll"><table><thead><tr>${favoriteHeader}<th>Name</th><th>Created by</th><th>Participants</th><th>Files</th><th>Revisions</th><th>Status</th><th>Updated</th><th class="notes-table-actions">Actions</th></tr></thead><tbody>${notes.map(note => `
<tr>
${accountSession ? `<td class="notes-table-favorite">${favoriteButton(note, true)}</td>` : ""}
<td><a class="note-table-link" href="${escapeHtml(safeAppUrl(note.url))}">${escapeHtml(note.title)}</a></td>
<td class="note-author">${escapeHtml(note.created_by || "Unknown")}</td>
<td>${Number(note.participant_count) || 0}</td>
@@ -199,11 +253,12 @@ function renderNotes(notes = notesCache) {
return;
}
notesList.innerHTML = notes.map(note => `
<article class="note-card-wrap">
<article class="note-card-wrap${accountSession ? " note-card-wrap--favorite-enabled" : ""}">
<a class="note-card" href="${escapeHtml(safeAppUrl(note.url))}">
<div class="note-card-title"><h3>${escapeHtml(note.title)}</h3>${note.protected ? '<span class="protect-badge">Protected</span>' : ''}</div>
<div class="note-card-meta"><span>Created by: ${escapeHtml(note.created_by || "Unknown")}</span>${noteStats(note)}<span>Updated: ${formatDate(note.updated_at)}</span></div>
</a>
${accountSession ? `<div class="note-card-favorite">${favoriteButton(note)}</div>` : ""}
${deleteButton(note)}
</article>`).join("");
}
@@ -323,6 +378,12 @@ document.querySelector("#note-form").addEventListener("submit", async e => {
} catch (err) { error.textContent = err.message; toast.danger(err.message, { title: "Could not create note" }); }
});
notesList.addEventListener("click", async event => {
const favorite = event.target.closest("[data-favorite-note]");
if (favorite) {
event.preventDefault();
await toggleNoteFavorite(favorite);
return;
}
const button = event.target.closest("[data-delete-note]");
if (!button) return;
const title = button.dataset.noteTitle;
@@ -350,6 +411,7 @@ document.querySelector("#copy-workspace-link").addEventListener("click", async (
});
async function startAuthorizedWorkspace() {
const session = await validateCurrentSession();
accountSession = session;
nickname = session?.nickname || getNickname();
if (!nickname) {
@@ -365,8 +427,9 @@ async function startAuthorizedWorkspace() {
bindIdentityDialog({
dialog: identityDialog,
onIdentity: async value => {
onIdentity: async (value, session) => {
nickname = value;
accountSession = session;
accessToken = shareToken || getAccessToken("workspace", slug);
workspaceContent.hidden = false;
await init();
@@ -386,6 +449,12 @@ dialog.addEventListener("cancel", event => {
}
});
window.addEventListener("rustpad:session-change", event => {
if (event.detail?.session) return;
if (!accountSession) return;
accountSession = null;
renderNotes();
});
window.addEventListener("beforeunload", stopWorkspaceWatch);
setNotesView(notesView);