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
+21 -11
View File
@@ -8,6 +8,7 @@
*/
import { logDebug, logError, logWarn } from "@rustpad/logger";
import { formatNumber, setLanguage, translateApiMessage } from "@rustpad/i18n";
const DEFAULT_ERRORS = {
400: "Invalid request.",
@@ -58,9 +59,9 @@ async function csrfToken({ refresh = false } = {}) {
}
function formatBytes(bytes) {
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(bytes % (1024 * 1024) ? 1 : 0)} MB`;
if (bytes >= 1024) return `${Math.ceil(bytes / 1024)} KB`;
return `${bytes} B`;
if (bytes >= 1024 * 1024) return `${formatNumber(bytes / (1024 * 1024), { maximumFractionDigits: bytes % (1024 * 1024) ? 1 : 0 })} MB`;
if (bytes >= 1024) return `${formatNumber(Math.ceil(bytes / 1024))} KB`;
return `${formatNumber(bytes)} B`;
}
function clearExpiredSession() {
@@ -69,7 +70,9 @@ function clearExpiredSession() {
sessionStorage.removeItem("rustpad:auth-token");
localStorage.removeItem("rustpad:nickname");
sessionStorage.removeItem("rustpad:nickname");
localStorage.removeItem("rustpad:language");
document.cookie = "rustpad_nickname=; Path=/; SameSite=Lax; Max-Age=0";
void setLanguage("en", { persist: false });
window.dispatchEvent(new CustomEvent("rustpad:session-expired"));
}
@@ -91,7 +94,9 @@ function validateUploadSize(body, configuredMaxBytes) {
if (!Number.isFinite(maxBytes) || maxBytes <= 0) return;
for (const value of body.values()) {
if (value instanceof File && value.size > maxBytes) {
const error = new Error(`The selected file is ${formatBytes(value.size)}. The upload limit is ${formatBytes(maxBytes)}.`);
const sourceMessage = `The selected file is ${formatBytes(value.size)}. The upload limit is ${formatBytes(maxBytes)}.`;
const error = new Error(translateApiMessage(sourceMessage));
error.serverMessage = sourceMessage;
error.status = 413;
throw error;
}
@@ -110,7 +115,9 @@ async function requestHeaders(options, body) {
}
function requestError(status, data = {}) {
const error = new Error(data.error || DEFAULT_ERRORS[status] || `Request failed (${status}).`);
const serverMessage = data.error || DEFAULT_ERRORS[status] || `Request failed (${status}).`;
const error = new Error(translateApiMessage(serverMessage));
error.serverMessage = serverMessage;
error.status = status;
return error;
}
@@ -153,11 +160,11 @@ export async function api(path, options = {}) {
} catch (error) {
if (error.name === "AbortError") {
logWarn("api.timeout", { method: options.method || "GET", path });
throw new Error("Timed out");
throw new Error(translateApiMessage("Timed out"));
}
logError("api.network_error", error, { method: options.method || "GET", path });
if (options.body instanceof FormData && error instanceof TypeError) {
throw new Error("Upload failed before the server returned a response. The file may exceed the server or proxy upload limit.");
throw new Error(translateApiMessage("Upload failed before the server returned a response. The file may exceed the server or proxy upload limit."));
}
throw error;
} finally {
@@ -261,15 +268,17 @@ export function uploadWithProgress(path, options = {}) {
reject(error);
});
xhr.addEventListener("error", () => {
const error = new Error("Upload failed before the server returned a response. Check the connection and try again.");
const error = new Error(translateApiMessage("Upload failed before the server returned a response. Check the connection and try again."));
logError("api.network_error", error, { method, path });
fail(error);
});
xhr.addEventListener("abort", () => {
const error = new Error(stalled
const sourceMessage = stalled
? "Upload stopped making progress. Check the connection and try again."
: responseTimedOut ? "The file was sent, but the server did not finish processing it. Try again."
: externallyAborted ? "Upload cancelled." : "Upload interrupted. Try again.");
: externallyAborted ? "Upload cancelled." : "Upload interrupted. Try again.";
const error = new Error(translateApiMessage(sourceMessage));
error.serverMessage = sourceMessage;
error.name = externallyAborted ? "AbortError" : "UploadError";
logWarn(stalled ? "api.upload_stalled" : responseTimedOut ? "api.upload_response_timeout" : "api.upload_aborted", { method, path });
fail(error);
@@ -278,7 +287,8 @@ export function uploadWithProgress(path, options = {}) {
if (options.signal) {
if (options.signal.aborted) {
externallyAborted = true;
const error = new Error("Upload cancelled.");
const error = new Error(translateApiMessage("Upload cancelled."));
error.serverMessage = "Upload cancelled.";
error.name = "AbortError";
fail(error);
return;