feat: add profile language preferences and refine toast, dropdown and history UI

This commit is contained in:
Mateusz Gruszczyński
2026-09-04 23:48:09 +02:00
parent f036240d5d
commit bf13587a71
42 changed files with 3971 additions and 357 deletions
+18 -11
View File
@@ -16,7 +16,10 @@ import { getNickname, getAccessToken, getAuthToken, getGuestId, setAccessToken }
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
import { askConfirm } from "@rustpad/modal";
import { isResourceAccessError, safeAppUrl } from "@rustpad/security";
import { toast } from "@rustpad/toast";
import { consumeQueuedToast, toast } from "@rustpad/toast";
import { formatDateTime, formatNumber } from "@rustpad/i18n";
consumeQueuedToast();
const parts = location.pathname.split("/").filter(Boolean);
const slug = parts[1];
@@ -135,12 +138,12 @@ function connectWorkspaceWatch() {
function escapeHtml(v) { const e = document.createElement("div"); e.textContent = v; return e.innerHTML; }
function formatBytes(value) {
const bytes = Math.max(0, Number(value) || 0);
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024) return `${formatNumber(bytes)} B`;
const units = ["KB", "MB", "GB", "TB"];
let amount = bytes;
let unit = -1;
do { amount /= 1024; unit++; } while (amount >= 1024 && unit < units.length - 1);
return `${amount >= 10 ? amount.toFixed(0) : amount.toFixed(1)} ${units[unit]}`;
return `${formatNumber(amount, { maximumFractionDigits: amount >= 10 ? 0 : 1 })} ${units[unit]}`;
}
function noteStats(note) {
return `<span>Participants: ${Number(note.participant_count) || 0}</span><span>Files: ${Number(note.file_count) || 0} (${formatBytes(note.file_size_bytes)})</span><span>Revisions: ${Number(note.revision_count) || 0}</span>`;
@@ -151,7 +154,7 @@ function formatDate(value) {
if (/^\d+$/.test(raw)) { const number = Number(raw); raw = raw.length <= 10 ? number * 1000 : number; }
else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(raw)) raw = raw.replace(" ", "T") + "Z";
const date = new Date(raw);
return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString("pl-PL");
return Number.isNaN(date.getTime()) ? "—" : formatDateTime(date);
}
function setNotesView(view) {
notesView = view === "table" ? "table" : "grid";
@@ -226,6 +229,7 @@ async function openWorkspace() {
const params = new URLSearchParams({ q: notesSearch.value.trim(), page: String(notesPage), per_page: notesPerPage.value });
const data = await api(`/api/workspaces/${encodeURIComponent(slug)}/open?${params}`, { method: "POST", body: JSON.stringify({ access_token: accessToken || null }) });
info = data.workspace;
document.querySelector("#workspace-title").removeAttribute("data-i18n");
document.querySelector("#workspace-title").textContent = info.title;
document.querySelector("#workspace-url").textContent = location.pathname;
document.title = `${info.title} · RustPad`;
@@ -250,6 +254,7 @@ async function init() {
try {
const headers = accessToken && accessToken !== "cookie" ? { Authorization: `Bearer ${accessToken}` } : {};
info = await api(`/api/workspaces/${encodeURIComponent(slug)}`, { headers });
document.querySelector("#workspace-title").removeAttribute("data-i18n");
document.querySelector("#workspace-title").textContent = info.title;
document.querySelector("#workspace-url").textContent = location.pathname;
updateWorkspacePasswordControl();
@@ -270,7 +275,8 @@ document.querySelector("#password-form").addEventListener("submit", async e => {
document.querySelector("#open-password").value = "";
document.querySelector("#password-error").textContent = "";
await openWorkspace();
} catch (error) { document.querySelector("#password-error").textContent = error.message; }
toast.success("Workspace access has been unlocked.", { title: "Workspace unlocked" });
} catch (error) { document.querySelector("#password-error").textContent = error.message; toast.danger(error.message, { title: "Could not unlock workspace" }); }
});
workspacePasswordForm.addEventListener("submit", async event => {
event.preventDefault();
@@ -294,10 +300,11 @@ workspacePasswordForm.addEventListener("submit", async event => {
setAccessToken("workspace", slug, result.granted);
accessToken = getAccessToken("workspace", slug);
workspacePasswordInput.value = "";
toast("Workspace password set");
toast.success("Password protection is now enabled for this workspace.", { title: "Workspace protected" });
await openWorkspace();
} catch (error) {
workspacePasswordError.textContent = error.message;
toast.danger(error.message, { title: "Could not set workspace password" });
} finally {
submit.disabled = false;
}
@@ -313,7 +320,7 @@ document.querySelector("#note-form").addEventListener("submit", async e => {
body: JSON.stringify({ name: document.querySelector("#note-name").value, access_token: accessToken || null, protect: document.querySelector("#note-protect").checked, created_by: nickname || null })
});
location.assign(safeAppUrl(note.url));
} catch (err) { error.textContent = err.message; }
} catch (err) { error.textContent = err.message; toast.danger(err.message, { title: "Could not create note" }); }
});
notesList.addEventListener("click", async event => {
const button = event.target.closest("[data-delete-note]");
@@ -325,9 +332,9 @@ notesList.addEventListener("click", async event => {
await api(`/api/workspaces/${encodeURIComponent(slug)}/notes/${encodeURIComponent(button.dataset.deleteNote)}`, {
method: "DELETE", body: JSON.stringify({ access_token: accessToken || null })
});
toast("Note deleted");
toast.success(`The note "${title}" was deleted.`, { title: "Note deleted" });
await openWorkspace();
} catch (error) { toast(error.message); button.disabled = false; }
} catch (error) { toast.danger(error.message, { title: "Could not delete note" }); button.disabled = false; }
});
document.querySelectorAll("[data-notes-view]").forEach(button => button.addEventListener("click", () => {
if (button.dataset.notesView === notesView) return;
@@ -338,8 +345,8 @@ notesSearch.addEventListener("input", () => { clearTimeout(notesSearchTimer); no
notesPerPage.addEventListener("change", () => { notesPage = 1; openWorkspace(); });
notesPagination.addEventListener("click", event => { const button = event.target.closest("[data-page]"); if (!button || button.disabled) return; notesPage = Number(button.dataset.page) || 1; openWorkspace(); });
document.querySelector("#copy-workspace-link").addEventListener("click", async () => {
try { await copyText(new URL(location.pathname, location.origin).href); toast("Link copied"); }
catch (e) { toast(e.message); }
try { await copyText(new URL(location.pathname, location.origin).href); toast.success("Workspace link copied to the clipboard.", { title: "Link copied" }); }
catch (e) { toast.danger(e.message, { title: "Could not copy workspace link" }); }
});
async function startAuthorizedWorkspace() {
const session = await validateCurrentSession();