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

This commit is contained in:
Mateusz Gruszczyński committed 2026-09-04 23:48:09 +02:00
1 parent f036240d5d
commit bf13587a71
42 files changed
+3971 -357

No files matched your search

+34 -2
View File
@@ -11,7 +11,7 @@ mod pages;
use axum::{
Json, Router,
extract::{DefaultBodyLimit, Request},
extract::{DefaultBodyLimit, Path, Request},
http::{HeaderMap, HeaderValue, Method, StatusCode, header},
middleware::{self, Next},
response::{IntoResponse, Response},
@@ -79,6 +79,8 @@ pub fn router(
.route("/w/{workspace_slug}", get(workspace))
.route("/w/{workspace_slug}/n/{note_slug}", get(note))
.route("/health", get(health))
.route("/lang", get(language_catalog))
.route("/lang/{code}", get(language_bundle))
.route("/robots.txt", get(robots_txt))
.route("/favicon.ico", get(favicon))
.route("/favicon.svg", get(favicon_svg))
@@ -240,6 +242,36 @@ pub fn router(
.with_state(state)
}
async fn language_catalog() -> Response {
let mut response = Json(crate::i18n::metadata()).into_response();
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=300"),
);
response
}
async fn language_bundle(Path(code): Path<String>) -> Response {
let Some(source) = crate::i18n::source(&code) else {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Language not found" })),
)
.into_response();
};
let mut response = source.into_response();
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json; charset=utf-8"),
);
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=86400"),
);
response
}
async fn require_csrf_token(request: Request, next: Next) -> Response {
let method = request.method();
let unsafe_method = method == Method::POST
@@ -324,7 +356,7 @@ fn is_file_path(path: &str) -> bool {
}
fn is_asset_path(path: &str) -> bool {
path == "/assets" || path.starts_with("/assets/")
path == "/assets" || path.starts_with("/assets/") || path == "/lang" || path.starts_with("/lang/")
}
fn is_icon_path(path: &str) -> bool {
+29 -9
View File
@@ -130,6 +130,7 @@ fn render_editor_page(
entrypoint: &str,
resource_kind: &str,
document_title: &str,
document_title_i18n: Option<&str>,
parent_title: &str,
parent_url: &str,
parent_class: &str,
@@ -156,6 +157,13 @@ fn render_editor_page(
.replace("__RESOURCE_ARIA_LABEL__", resource_aria_label)
.replace("__RESOURCE_BREADCRUMB__", &resource_breadcrumb)
.replace("__DOCUMENT_TITLE__", &escape_html(document_title))
.replace(
"__DOCUMENT_TITLE_I18N__",
document_title_i18n
.map(|key| format!(r#"data-i18n="{}""#, escape_html(key)))
.as_deref()
.unwrap_or(""),
)
.replace("__PARENT_TITLE__", &escape_html(parent_title))
.replace("__PARENT_URL__", &escape_html(parent_url))
.replace("__PARENT_CLASS__", parent_class)
@@ -289,6 +297,7 @@ pub(super) async fn pad(
"pad",
"pad",
&pad.title,
None,
"RustPad",
"/",
"home-brand",
@@ -363,14 +372,19 @@ pub(super) async fn workspace(
)
.await;
}
let html = include_str!("../../static/workspace.html").replace(
"__WORKSPACE_TITLE__",
&escape_html(if workspace.is_private != 0 {
"Workspace"
} else {
&workspace.title
}),
);
let html = include_str!("../../static/workspace.html")
.replace(
"__WORKSPACE_TITLE__",
&escape_html(if workspace.is_private != 0 {
"Workspace"
} else {
&workspace.title
}),
)
.replace(
"__WORKSPACE_TITLE_I18N__",
if workspace.is_private != 0 { r#"data-i18n="common.workspace""# } else { "" },
);
assets::render_html(
&html,
&state.asset_version,
@@ -446,6 +460,7 @@ pub(super) async fn note(
} else {
&note.title
},
if workspace.is_private != 0 { Some("common.note") } else { None },
if workspace.is_private != 0 {
"Workspace"
} else {
@@ -531,11 +546,16 @@ pub(super) fn error_response(
asset_version: &str,
) -> Response {
let html = include_str!("../../static/error.html")
.replace("__APP_THEME_BOOTSTRAP__", assets::theme_bootstrap())
.replace(
"__APP_THEME_BOOTSTRAP__",
&format!("{}{}", assets::theme_bootstrap(), assets::language_bootstrap()),
)
.replace(
"__APP_STYLESHEET__",
&assets::stylesheet_tag(asset_version, "styles"),
)
.replace("__APP_IMPORT_MAP__", &assets::import_map_tag(asset_version))
.replace("__APP_I18N__", assets::i18n_entrypoint())
.replace("__ERROR_CODE__", &escape_html(code))
.replace("__ERROR_TITLE__", &escape_html(title))
.replace("__ERROR_MESSAGE__", &escape_html(message))