//! 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//`. 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(template: &T) -> String { template .render() .unwrap_or_else(|error| format!("

Template error: {error}

")) }