Jump to…
snowfix: 500 error because of database is patchedyuzpxzopsouq1mo
Matt W1//! Maud templates.
Matt W2//!
Matt W3//! Every fragment that htmx can swap is also a function that renders
Matt W4//! standalone, so each URL works without JavaScript (spec §7).
Matt W5
Matt W6pub mod change;
Matt W7pub mod design;
Matt W8pub mod diff;
Matt W9pub mod edit;
Matt W10pub mod issue;
Matt W11pub mod layout;
Matt W12pub mod pages;
Matt W13pub mod repo;
Matt W14pub mod review;
Matt W15pub mod settings;
Matt W16
Matt W17pub use layout::{error_page, page, page_full, page_with_bar, Chrome};
Matt W18
Matt W19use chrono::{DateTime, Utc};
Matt W20use maud::{html, Markup};
Matt W21
Matt W22/// A Dogfood handle, linked to its profile.
Matt W23///
Matt W24/// Every handle the UI renders is a person with a page, so linking is the
Matt W25/// default rather than something each call site decides. Kept here rather than
Matt W26/// in one view module because comments, reviews, timelines, and listings all
Matt W27/// need it and none of them owns the concept.
Matt W28pub fn user_link(handle: &str) -> Markup {
Matt W29 html! {
Matt W30 a .user-link href=(format!("/{handle}")) { (handle) }
Matt W31 }
Matt W32}
Matt W33
Matt W34/// A person who may or may not have an account.
Matt W35///
Matt W36/// `handle` links; a bare commit name renders as text with a tooltip saying
Matt W37/// why it does not. Only a commit with no name at all falls through to
Matt W38/// "someone" — see `pages::actor`, which this generalises.
Matt W39pub fn person(handle: Option<&str>, name: Option<&str>) -> Markup {
Matt W40 html! {
Matt W41 @match (handle, name.map(str::trim).filter(|n| !n.is_empty())) {
Matt W42 (Some(h), _) => (user_link(h)),
Matt W43 (None, Some(n)) => span title="this commit is not linked to a Dogfood account" { (n) },
Matt W44 (None, None) => span .faint { "someone" },
Matt W45 }
Matt W46 }
Matt W47}
Matt W48
Matt W49/// A short, coarse "how long ago" label.
Matt W50///
Matt W51/// Feeds and file listings in the design are scanned, not read — a reader wants
Matt W52/// to know whether something is fresh, not the minute it landed. Coarse buckets
Matt W53/// say that faster than a timestamp does, so the exact time goes in a `title`
Matt W54/// attribute at the call site for anyone who needs it.
Matt W55///
Matt W56/// Deliberately has no "just now" case below a minute: a feed row that says
Matt W57/// "0m" reads as broken, and one that says "just now" goes stale the moment the
Matt W58/// page is cached.
Matt W59pub fn relative_time(then: DateTime<Utc>, now: DateTime<Utc>) -> String {
Matt W60 let secs = (now - then).num_seconds();
Matt W61
Matt W62 // A clock skew between the app and the database should not render as a
Matt W63 // date in the future.
Matt W64 if secs <= 0 {
Matt W65 return "now".into();
Matt W66 }
Matt W67
Matt W68 const MINUTE: i64 = 60;
Matt W69 const HOUR: i64 = 60 * MINUTE;
Matt W70 const DAY: i64 = 24 * HOUR;
Matt W71 const MONTH: i64 = 30 * DAY;
Matt W72 const YEAR: i64 = 365 * DAY;
Matt W73
Matt W74 match secs {
Matt W75 s if s < MINUTE => "now".into(),
Matt W76 s if s < HOUR => format!("{}m", s / MINUTE),
Matt W77 s if s < DAY => format!("{}h", s / HOUR),
Matt W78 s if s < MONTH => format!("{}d", s / DAY),
Matt W79 s if s < YEAR => format!("{}mo", s / MONTH),
Matt W80 s => format!("{}y", s / YEAR),
Matt W81 }
Matt W82}
Matt W83
Matt W84#[cfg(test)]
Matt W85mod relative_time_tests {
Matt W86 use super::relative_time;
Matt W87 use chrono::{Duration, Utc};
Matt W88
Matt W89 #[test]
Matt W90 fn buckets_read_as_labels() {
Matt W91 let now = Utc::now();
Matt W92 let ago = |d: Duration| relative_time(now - d, now);
Matt W93
Matt W94 assert_eq!(ago(Duration::seconds(5)), "now");
Matt W95 assert_eq!(ago(Duration::minutes(3)), "3m");
Matt W96 assert_eq!(ago(Duration::hours(5)), "5h");
Matt W97 assert_eq!(ago(Duration::days(9)), "9d");
Matt W98 assert_eq!(ago(Duration::days(70)), "2mo");
Matt W99 assert_eq!(ago(Duration::days(800)), "2y");
Matt W100 }
Matt W101
Matt W102 /// A row timestamped slightly in the future is a clock skew, not a
Matt W103 /// scheduled post — it must not render as one.
Matt W104 #[test]
Matt W105 fn a_future_timestamp_does_not_render_as_negative() {
Matt W106 let now = Utc::now();
Matt W107 assert_eq!(relative_time(now + Duration::hours(3), now), "now");
Matt W108 }
Matt W109}

109 lines · Rust