mirror of
https://github.com/perstarkse/minne.git
synced 2026-05-13 19:30:42 +02:00
feat: add user theme preference
- Add theme field to User model (common) - Create migration for theme field - Add theme selection to Account Settings (html-router) - Implement server-side theme rendering in base template - Update JS for system/preference theme handling - Remove header theme toggle for authenticated users
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
extract::{Request, State},
|
||||
http::{HeaderName, StatusCode},
|
||||
middleware::Next,
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
Extension,
|
||||
};
|
||||
@@ -11,6 +12,8 @@ use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use tracing::error;
|
||||
|
||||
use crate::AuthSessionType;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum TemplateKind {
|
||||
Full(String),
|
||||
@@ -98,14 +101,40 @@ impl IntoResponse for TemplateResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ContextWrapper<'a> {
|
||||
user_theme: &'a str,
|
||||
initial_theme: &'a str,
|
||||
is_authenticated: bool,
|
||||
#[serde(flatten)]
|
||||
context: &'a Value,
|
||||
}
|
||||
|
||||
pub async fn with_template_response<S>(
|
||||
State(state): State<S>,
|
||||
HxRequest(is_htmx): HxRequest,
|
||||
response: Response<axum::body::Body>,
|
||||
) -> Response<axum::body::Body>
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Response
|
||||
where
|
||||
S: ProvidesTemplateEngine + Clone + Send + Sync + 'static,
|
||||
{
|
||||
// Determine theme context
|
||||
let (user_theme, initial_theme, is_authenticated) =
|
||||
if let Some(auth) = req.extensions().get::<AuthSessionType>() {
|
||||
if let Some(user) = &auth.current_user {
|
||||
let theme = user.theme.as_str();
|
||||
let initial = if theme == "dark" { "dark" } else { "light" };
|
||||
(theme.to_string(), initial.to_string(), true)
|
||||
} else {
|
||||
("system".to_string(), "light".to_string(), false)
|
||||
}
|
||||
} else {
|
||||
("system".to_string(), "light".to_string(), false)
|
||||
};
|
||||
|
||||
let response = next.run(req).await;
|
||||
|
||||
// Headers to forward from the original response
|
||||
const HTMX_HEADERS_TO_FORWARD: &[&str] = &["HX-Push", "HX-Trigger", "HX-Redirect"];
|
||||
|
||||
@@ -123,9 +152,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let context = ContextWrapper {
|
||||
user_theme: &user_theme,
|
||||
initial_theme: &initial_theme,
|
||||
is_authenticated,
|
||||
context: &template_response.context,
|
||||
};
|
||||
|
||||
match &template_response.template_kind {
|
||||
TemplateKind::Full(name) => {
|
||||
match template_engine.render(name, &template_response.context) {
|
||||
match template_engine.render(name, &Value::from_serialize(&context)) {
|
||||
Ok(html) => {
|
||||
let mut final_response = Html(html).into_response();
|
||||
forward_headers(response.headers(), final_response.headers_mut());
|
||||
@@ -138,7 +174,11 @@ where
|
||||
}
|
||||
}
|
||||
TemplateKind::Partial(template, block) => {
|
||||
match template_engine.render_block(template, block, &template_response.context) {
|
||||
match template_engine.render_block(
|
||||
template,
|
||||
block,
|
||||
&Value::from_serialize(&context),
|
||||
) {
|
||||
Ok(html) => {
|
||||
let mut final_response = Html(html).into_response();
|
||||
forward_headers(response.headers(), final_response.headers_mut());
|
||||
@@ -169,12 +209,15 @@ where
|
||||
let trigger_payload = json!({"toast": {"title": title, "description": description, "type": "error"}});
|
||||
let trigger_value = serde_json::to_string(&trigger_payload).unwrap_or_else(|e| {
|
||||
error!("Failed to serialize HX-Trigger payload: {}", e);
|
||||
r#"{"toast":{"title":"Error","description":"An unexpected error occurred.", "type":"error"}}"#.to_string()
|
||||
r#"{"toast":{"title":"Error","description":"An unexpected error occurred.", "type":"error"}}"#
|
||||
.to_string()
|
||||
});
|
||||
(StatusCode::NO_CONTENT, [(HX_TRIGGER, trigger_value)], "").into_response()
|
||||
} else {
|
||||
// Non-HTMX request: Render the full errors/error.html page
|
||||
match template_engine.render("errors/error.html", &template_response.context) {
|
||||
match template_engine
|
||||
.render("errors/error.html", &Value::from_serialize(&context))
|
||||
{
|
||||
Ok(html) => (*status, Html(html)).into_response(),
|
||||
Err(e) => {
|
||||
error!("Critical: Failed to render 'errors/error.html': {:?}", e);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use axum::{
|
||||
extract::FromRef,
|
||||
middleware::{from_fn_with_state, map_response_with_state},
|
||||
Router,
|
||||
};
|
||||
use axum::{extract::FromRef, middleware::from_fn_with_state, Router};
|
||||
use axum_session::SessionLayer;
|
||||
use axum_session_auth::{AuthConfig, AuthSessionLayer};
|
||||
use axum_session_surreal::SessionSurrealPool;
|
||||
@@ -181,7 +177,7 @@ where
|
||||
self.app_state.clone(),
|
||||
analytics_middleware::<HtmlState>,
|
||||
));
|
||||
router = router.layer(map_response_with_state(
|
||||
router = router.layer(from_fn_with_state(
|
||||
self.app_state.clone(),
|
||||
with_template_response::<HtmlState>,
|
||||
));
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct AccountPageData {
|
||||
user: User,
|
||||
timezones: Vec<String>,
|
||||
conversation_archive: Vec<Conversation>,
|
||||
theme_options: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn show_account_page(
|
||||
@@ -28,6 +29,11 @@ pub async fn show_account_page(
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect();
|
||||
let theme_options = vec![
|
||||
"light".to_string(),
|
||||
"dark".to_string(),
|
||||
"system".to_string(),
|
||||
];
|
||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||
|
||||
Ok(TemplateResponse::new_template(
|
||||
@@ -36,6 +42,7 @@ pub async fn show_account_page(
|
||||
user,
|
||||
timezones,
|
||||
conversation_archive,
|
||||
theme_options,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -65,6 +72,7 @@ pub async fn set_api_key(
|
||||
user: updated_user,
|
||||
timezones: vec![],
|
||||
conversation_archive: vec![],
|
||||
theme_options: vec![],
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -118,6 +126,47 @@ pub async fn update_timezone(
|
||||
user: updated_user,
|
||||
timezones,
|
||||
conversation_archive: vec![],
|
||||
theme_options: vec![],
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateThemeForm {
|
||||
theme: String,
|
||||
}
|
||||
|
||||
pub async fn update_theme(
|
||||
State(state): State<HtmlState>,
|
||||
RequireUser(user): RequireUser,
|
||||
auth: AuthSessionType,
|
||||
Form(form): Form<UpdateThemeForm>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
User::update_theme(&user.id, &form.theme, &state.db).await?;
|
||||
|
||||
// Clear the cache
|
||||
auth.cache_clear_user(user.id.to_string());
|
||||
|
||||
// Update the user's theme
|
||||
let updated_user = User {
|
||||
theme: form.theme,
|
||||
..user.clone()
|
||||
};
|
||||
|
||||
let theme_options = vec![
|
||||
"light".to_string(),
|
||||
"dark".to_string(),
|
||||
"system".to_string(),
|
||||
];
|
||||
|
||||
Ok(TemplateResponse::new_partial(
|
||||
"auth/account_settings.html",
|
||||
"theme_section",
|
||||
AccountPageData {
|
||||
user: updated_user,
|
||||
timezones: vec![],
|
||||
conversation_archive: vec![],
|
||||
theme_options,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ where
|
||||
.route("/account", get(handlers::show_account_page))
|
||||
.route("/set-api-key", post(handlers::set_api_key))
|
||||
.route("/update-timezone", patch(handlers::update_timezone))
|
||||
.route("/update-theme", patch(handlers::update_theme))
|
||||
.route(
|
||||
"/change-password",
|
||||
get(handlers::show_change_password).patch(handlers::change_password),
|
||||
|
||||
@@ -45,7 +45,15 @@ pub async fn process_signup_and_show_verification(
|
||||
auth: AuthSessionType,
|
||||
Form(form): Form<SignupParams>,
|
||||
) -> Result<impl IntoResponse, HtmlError> {
|
||||
let user = match User::create_new(form.email, form.password, &state.db, form.timezone).await {
|
||||
let user = match User::create_new(
|
||||
form.email,
|
||||
form.password,
|
||||
&state.db,
|
||||
form.timezone,
|
||||
"system".to_string(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(user) => user,
|
||||
Err(e) => {
|
||||
tracing::error!("{:?}", e);
|
||||
|
||||
Reference in New Issue
Block a user