From c000ac07b3a8856eb3d460ad04572b2c87f5466f Mon Sep 17 00:00:00 2001 From: Per Stark Date: Tue, 8 Apr 2025 15:45:16 +0200 Subject: [PATCH] refactor: uniform toast and better error display --- html-router/assets/toast.js | 56 +++++++++++++++++ .../src/middlewares/response_middleware.rs | 62 +++++++++++++------ .../templates/auth/account_settings.html | 37 ++--------- html-router/templates/body_base.html | 1 + html-router/templates/head_base.html | 7 +-- 5 files changed, 106 insertions(+), 57 deletions(-) create mode 100644 html-router/assets/toast.js diff --git a/html-router/assets/toast.js b/html-router/assets/toast.js new file mode 100644 index 0000000..bed070a --- /dev/null +++ b/html-router/assets/toast.js @@ -0,0 +1,56 @@ + document.addEventListener("DOMContentLoaded", function () { + window.show_toast = function (description, type = 'info', title = null) { + const container = document.getElementById('toast-container'); + if (!container) { + console.error("Toast container not found!"); + return; + } + const alert = document.createElement('div'); + // Base classes for the alert + alert.className = `alert alert-${type} mt-2 shadow-md flex flex-col text-start`; + + // Build inner HTML based on whether title is provided + let innerHTML = ''; + if (title) { + innerHTML += `
${title}
`; // Title element + innerHTML += `
${description}
`; // Description element + } else { + // Structure without title + innerHTML += `${description}`; + } + + alert.innerHTML = innerHTML; + container.appendChild(alert); + + // Auto-remove after a delay + setTimeout(() => { + // Optional: Add fade-out effect + alert.style.opacity = '0'; + alert.style.transition = 'opacity 0.5s ease-out'; + setTimeout(() => alert.remove(), 500); // Remove after fade + }, 3000); // Start fade-out after 3 seconds + }; + + document.body.addEventListener('toast', function (event) { + // Extract data from the event detail, matching the Rust payload + const detail = event.detail; + if (detail && detail.description) { + const description = detail.description; + const type = detail.type || 'info'; // Default to 'info' + const title = detail.title || null; // Get title, default to null if missing + + // Call the updated show_toast function + window.show_toast(description, type, title); + } else { + console.warn("Received toast event without detail.description", detail); + // Fallback toast if description is missing + window.show_toast("An event occurred, but details are missing.", "warning"); + } + }); + + document.body.addEventListener('htmx:beforeRequest', function (evt) { + const container = document.getElementById('toast-container'); + if (container) container.innerHTML = ''; + }); + }) + diff --git a/html-router/src/middlewares/response_middleware.rs b/html-router/src/middlewares/response_middleware.rs index 203af37..1e59fc3 100644 --- a/html-router/src/middlewares/response_middleware.rs +++ b/html-router/src/middlewares/response_middleware.rs @@ -7,6 +7,7 @@ use axum::{ use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine}; use minijinja::{context, Value}; use serde::Serialize; +use serde_json::json; use tracing::error; #[derive(Clone)] @@ -42,11 +43,10 @@ impl TemplateResponse { } } - pub fn error(status: StatusCode, title: &str, error: &str, description: &str) -> Self { + pub fn error(status: StatusCode, title: &str, description: &str) -> Self { let ctx = context! { status_code => status.as_u16(), title => title, - error => error, description => description }; Self { @@ -59,7 +59,6 @@ impl TemplateResponse { Self::error( StatusCode::NOT_FOUND, "Page Not Found", - "Not Found", "The page you're looking for doesn't exist or was removed.", ) } @@ -68,7 +67,6 @@ impl TemplateResponse { Self::error( StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error", - "Internal Server Error", "Something went wrong on our end.", ) } @@ -77,18 +75,12 @@ impl TemplateResponse { Self::error( StatusCode::UNAUTHORIZED, "Unauthorized", - "Access Denied", "You need to be logged in to access this page.", ) } pub fn bad_request(message: &str) -> Self { - Self::error( - StatusCode::BAD_REQUEST, - "Bad Request", - "Bad Request", - message, - ) + Self::error(StatusCode::BAD_REQUEST, "Bad Request", message) } pub fn redirect(path: impl Into) -> Self { @@ -118,7 +110,7 @@ where Ok(html) => Html(html).into_response(), Err(e) => { error!("Failed to render template '{}': {:?}", name, e); - (StatusCode::INTERNAL_SERVER_ERROR, fallback_error()).into_response() + (StatusCode::INTERNAL_SERVER_ERROR, Html(fallback_error())).into_response() } } } @@ -127,18 +119,48 @@ where Ok(html) => Html(html).into_response(), Err(e) => { error!("Failed to render block '{}/{}': {:?}", template, block, e); - (StatusCode::INTERNAL_SERVER_ERROR, fallback_error()).into_response() + (StatusCode::INTERNAL_SERVER_ERROR, Html(fallback_error())).into_response() } } } - TemplateKind::Error(status) => { - match template_engine.render("errors/error.html", &template_response.context) { - Ok(html) => (*status, Html(html)).into_response(), - Err(e) => { - error!("Failed to render error template: {:?}", e); - (*status, fallback_error()).into_response() + TemplateKind::Error(_status) => { + // Extract title and description from context + let title = template_response + .context + .get_attr("title") + .ok() + .and_then(|v| v.as_str().map(|s| s.to_string())) + .unwrap_or_else(|| "Error".to_string()); // Fallback title + let description = template_response + .context + .get_attr("description") + .ok() + .and_then(|v| v.as_str().map(|s| s.to_string())) + .unwrap_or_else(|| "An error occurred.".to_string()); // Fallback desc + + let trigger_payload = json!({ + "toast": { + "title": title, + "description": description, + "type": "error" } - } + }); + + // Convert payload to string + let trigger_value = serde_json::to_string(&trigger_payload) + .unwrap_or_else(|e| { + error!("Failed to serialize HX-Trigger payload: {}", e); + // Fallback trigger if serialization fails + r#"{"toast":{"title":"Error","description":"An unexpected error occurred.", "type":"error"}}"#.to_string() + }); + + // Return 204 No Content with HX-Trigger header + ( + StatusCode::NO_CONTENT, + [(axum_htmx::HX_TRIGGER, trigger_value)], + "", // Empty body for 204 + ) + .into_response() } TemplateKind::Redirect(path) => { (StatusCode::OK, [(axum_htmx::HX_REDIRECT, path.clone())], "").into_response() diff --git a/html-router/templates/auth/account_settings.html b/html-router/templates/auth/account_settings.html index 11c493a..6512b26 100644 --- a/html-router/templates/auth/account_settings.html +++ b/html-router/templates/auth/account_settings.html @@ -40,44 +40,15 @@ diff --git a/html-router/templates/body_base.html b/html-router/templates/body_base.html index 482c8c9..2497dc5 100644 --- a/html-router/templates/body_base.html +++ b/html-router/templates/body_base.html @@ -10,6 +10,7 @@ {% block main %}{% endblock %} +
{% endblock %} \ No newline at end of file diff --git a/html-router/templates/head_base.html b/html-router/templates/head_base.html index 8ddeb0e..d275fb2 100644 --- a/html-router/templates/head_base.html +++ b/html-router/templates/head_base.html @@ -6,8 +6,6 @@ {% block title %}Minne{% endblock %} - - @@ -20,6 +18,7 @@ + @@ -33,8 +32,6 @@ {% block head %}{% endblock %} -{% block body %}{% endblock %} - +{% block body %}{% endblock %} + \ No newline at end of file