mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-23 09:18:36 +02:00
refactor: uniform toast and better error display
This commit is contained in:
56
html-router/assets/toast.js
Normal file
56
html-router/assets/toast.js
Normal file
@@ -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 += `<div class="font-bold text-lg">${title}</div>`; // Title element
|
||||||
|
innerHTML += `<div>${description}</div>`; // Description element
|
||||||
|
} else {
|
||||||
|
// Structure without title
|
||||||
|
innerHTML += `<span>${description}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '';
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ use axum::{
|
|||||||
use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine};
|
use common::{error::AppError, utils::template_engine::ProvidesTemplateEngine};
|
||||||
use minijinja::{context, Value};
|
use minijinja::{context, Value};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde_json::json;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[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! {
|
let ctx = context! {
|
||||||
status_code => status.as_u16(),
|
status_code => status.as_u16(),
|
||||||
title => title,
|
title => title,
|
||||||
error => error,
|
|
||||||
description => description
|
description => description
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
@@ -59,7 +59,6 @@ impl TemplateResponse {
|
|||||||
Self::error(
|
Self::error(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"Page Not Found",
|
"Page Not Found",
|
||||||
"Not Found",
|
|
||||||
"The page you're looking for doesn't exist or was removed.",
|
"The page you're looking for doesn't exist or was removed.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -68,7 +67,6 @@ impl TemplateResponse {
|
|||||||
Self::error(
|
Self::error(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"Internal Server Error",
|
"Internal Server Error",
|
||||||
"Internal Server Error",
|
|
||||||
"Something went wrong on our end.",
|
"Something went wrong on our end.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -77,18 +75,12 @@ impl TemplateResponse {
|
|||||||
Self::error(
|
Self::error(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
"Unauthorized",
|
"Unauthorized",
|
||||||
"Access Denied",
|
|
||||||
"You need to be logged in to access this page.",
|
"You need to be logged in to access this page.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bad_request(message: &str) -> Self {
|
pub fn bad_request(message: &str) -> Self {
|
||||||
Self::error(
|
Self::error(StatusCode::BAD_REQUEST, "Bad Request", message)
|
||||||
StatusCode::BAD_REQUEST,
|
|
||||||
"Bad Request",
|
|
||||||
"Bad Request",
|
|
||||||
message,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redirect(path: impl Into<String>) -> Self {
|
pub fn redirect(path: impl Into<String>) -> Self {
|
||||||
@@ -118,7 +110,7 @@ where
|
|||||||
Ok(html) => Html(html).into_response(),
|
Ok(html) => Html(html).into_response(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to render template '{}': {:?}", name, 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(),
|
Ok(html) => Html(html).into_response(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to render block '{}/{}': {:?}", template, block, 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) => {
|
TemplateKind::Error(_status) => {
|
||||||
match template_engine.render("errors/error.html", &template_response.context) {
|
// Extract title and description from context
|
||||||
Ok(html) => (*status, Html(html)).into_response(),
|
let title = template_response
|
||||||
Err(e) => {
|
.context
|
||||||
error!("Failed to render error template: {:?}", e);
|
.get_attr("title")
|
||||||
(*status, fallback_error()).into_response()
|
.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) => {
|
TemplateKind::Redirect(path) => {
|
||||||
(StatusCode::OK, [(axum_htmx::HX_REDIRECT, path.clone())], "").into_response()
|
(StatusCode::OK, [(axum_htmx::HX_REDIRECT, path.clone())], "").into_response()
|
||||||
|
|||||||
@@ -40,44 +40,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Using a single toast element avoids creating many timeouts when clicking repeatedly.
|
|
||||||
let current_toast = null;
|
|
||||||
let toast_timeout = null;
|
|
||||||
|
|
||||||
function show_toast(message) {
|
|
||||||
if (current_toast) {
|
|
||||||
// Update message and reset timeout if a toast is already displayed.
|
|
||||||
current_toast.querySelector('span').textContent = message;
|
|
||||||
clearTimeout(toast_timeout);
|
|
||||||
} else {
|
|
||||||
current_toast = document.createElement('div');
|
|
||||||
current_toast.className = 'toast';
|
|
||||||
current_toast.innerHTML = `<div class="alert alert-success">
|
|
||||||
<div>
|
|
||||||
<span>${message}</span>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
document.body.appendChild(current_toast);
|
|
||||||
}
|
|
||||||
toast_timeout = setTimeout(() => {
|
|
||||||
if (current_toast) {
|
|
||||||
current_toast.remove();
|
|
||||||
current_toast = null;
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function copy_api_key() {
|
function copy_api_key() {
|
||||||
const input = document.getElementById('api_key_input');
|
const input = document.getElementById('api_key_input');
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
navigator.clipboard.writeText(input.value).then(() => {
|
navigator.clipboard.writeText(input.value)
|
||||||
show_toast('API key copied!');
|
.then(() => show_toast('API key copied!', 'success'))
|
||||||
}).catch(() => {
|
.catch(() => show_toast('Copy failed', 'error'));
|
||||||
show_toast('Copy failed');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
show_toast('Copy not supported');
|
show_toast('Copy not supported', 'info');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
{% block main %}{% endblock %}
|
{% block main %}{% endblock %}
|
||||||
|
|
||||||
<div id="modal"></div>
|
<div id="modal"></div>
|
||||||
|
<div id="toast-container" class="toast toast-bottom toast-end"></div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -6,8 +6,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<title>{% block title %}Minne{% endblock %}</title>
|
<title>{% block title %}Minne{% endblock %}</title>
|
||||||
|
|
||||||
<!-- <meta http-equiv=" refresh" content="4"> -->
|
|
||||||
|
|
||||||
<!-- Preload critical assets -->
|
<!-- Preload critical assets -->
|
||||||
<link rel="preload" href="/assets/htmx.min.js" as="script">
|
<link rel="preload" href="/assets/htmx.min.js" as="script">
|
||||||
<link rel="preload" href="/assets/htmx-ext-sse.js" as="script">
|
<link rel="preload" href="/assets/htmx-ext-sse.js" as="script">
|
||||||
@@ -20,6 +18,7 @@
|
|||||||
<script src="/assets/htmx.min.js" defer></script>
|
<script src="/assets/htmx.min.js" defer></script>
|
||||||
<script src="/assets/htmx-ext-sse.js" defer></script>
|
<script src="/assets/htmx-ext-sse.js" defer></script>
|
||||||
<script src="/assets/theme-toggle.js" defer></script>
|
<script src="/assets/theme-toggle.js" defer></script>
|
||||||
|
<script src="/assets/toast.js" defer></script>
|
||||||
|
|
||||||
<!-- Icons -->
|
<!-- Icons -->
|
||||||
<link rel="icon" href="/assets/icon/favicon.ico">
|
<link rel="icon" href="/assets/icon/favicon.ico">
|
||||||
@@ -33,8 +32,6 @@
|
|||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
{% block body %}{% endblock %}
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function wait_for_htmx() {
|
(function wait_for_htmx() {
|
||||||
if (window.htmx) {
|
if (window.htmx) {
|
||||||
@@ -45,4 +42,6 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user