webs unified

This commit is contained in:
Priec
2026-08-03 13:32:25 +02:00
parent 35d85de556
commit c8afe99d79
59 changed files with 1991 additions and 952 deletions

119
web/src/ui/mod.rs Normal file
View 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>"))
}