webs unified
This commit is contained in:
119
web/src/ui/mod.rs
Normal file
119
web/src/ui/mod.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! The page shell: navbar, layouts, and the status blocks every page reuses.
|
||||
//!
|
||||
//! `templates/` mirrors `src/` directory for directory, so this module's
|
||||
//! markup is in `templates/ui/` and a page's markup is in
|
||||
//! `templates/<the page's path under src>/`.
|
||||
|
||||
use askama::Template;
|
||||
use axum::http::HeaderMap;
|
||||
|
||||
pub(crate) const SESSION_COOKIE: &str = "analytics_token";
|
||||
|
||||
/// Navbar state. Every full-page template struct carries one of these, because
|
||||
/// `ui/base.html` renders `ui/navbar.html` unconditionally.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Nav {
|
||||
pub authenticated: bool,
|
||||
pub role: String,
|
||||
pub active: &'static str,
|
||||
}
|
||||
|
||||
impl Nav {
|
||||
/// `active` is the nav link to highlight: `"admin"`, `"analytics"`,
|
||||
/// `"login"`, or `""` for pages that are not themselves nav entries.
|
||||
pub(crate) fn new(headers: &HeaderMap, active: &'static str) -> Self {
|
||||
Self {
|
||||
authenticated: crate::cookie_value(headers, SESSION_COOKIE).is_some(),
|
||||
role: String::new(),
|
||||
active,
|
||||
}
|
||||
}
|
||||
|
||||
/// The admin page is the only one that learns the caller's role, so it is
|
||||
/// the only one that can show the role badge.
|
||||
pub(crate) fn with_role(mut self, role: String) -> Self {
|
||||
self.role = role;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Nav {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
authenticated: false,
|
||||
role: String::new(),
|
||||
active: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The swap target every form POST answers with.
|
||||
#[derive(Template)]
|
||||
#[template(path = "ui/alert_fragment.html")]
|
||||
pub(crate) struct Alert<'a> {
|
||||
success: bool,
|
||||
title: &'a str,
|
||||
message: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Alert<'a> {
|
||||
pub(crate) fn error(title: &'a str, message: &'a str) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
title,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn success(title: &'a str, message: &'a str) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
title,
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-line notice, for places where the alert card is too heavy.
|
||||
#[derive(Template)]
|
||||
#[template(path = "ui/notice.html")]
|
||||
pub(crate) struct Notice<'a> {
|
||||
error: bool,
|
||||
message: &'a str,
|
||||
login_link: bool,
|
||||
}
|
||||
|
||||
impl<'a> Notice<'a> {
|
||||
pub(crate) fn error(message: &'a str) -> Self {
|
||||
Self {
|
||||
error: true,
|
||||
message,
|
||||
login_link: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn login_required(message: &'a str) -> Self {
|
||||
Self {
|
||||
error: true,
|
||||
message,
|
||||
login_link: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Standalone page for load failures that are not worth a redirect.
|
||||
#[derive(Template)]
|
||||
#[template(path = "ui/error.html")]
|
||||
pub(crate) struct ErrorPage<'a> {
|
||||
pub nav: Nav,
|
||||
pub heading: &'a str,
|
||||
pub message: &'a str,
|
||||
}
|
||||
|
||||
/// Renders a template, or a plain error paragraph if the template itself
|
||||
/// fails. Askama only fails on `fmt` errors, so this is a formality.
|
||||
pub(crate) fn render<T: Template>(template: &T) -> String {
|
||||
template
|
||||
.render()
|
||||
.unwrap_or_else(|error| format!("<p class=\"error\">Template error: {error}</p>"))
|
||||
}
|
||||
Reference in New Issue
Block a user