max / makenotwork
1 file changed,
+196 insertions,
-0 deletions
| @@ -1,0 +1,196 @@ | |||
| 1 | + | //! The team page at `/team`, described. | |
| 2 | + | //! | |
| 3 | + | //! The first described document this server serves to a reader who may or may | |
| 4 | + | //! not hold a session, which is the question `b5cbb646` left open and | |
| 5 | + | //! [`super::Audience`] answers. It replaces `templates/pages/team.html`, | |
| 6 | + | //! `TeamTemplate` and `landing::team_page`. | |
| 7 | + | //! | |
| 8 | + | //! # Why this is the screen that answers the signed-out question | |
| 9 | + | //! | |
| 10 | + | //! The gated documents (`/feed`, `/dashboard/export`) resolve a session and | |
| 11 | + | //! refuse without one. `/pricing` resolves nothing and takes a state built once | |
| 12 | + | //! at startup, which is why it carries no site header: with no per-request | |
| 13 | + | //! session there is nobody for a header to greet. | |
| 14 | + | //! | |
| 15 | + | //! Every remaining `pages/` template is neither. They are public, they carry | |
| 16 | + | //! the header, and the header reads differently once a reader signs in -- | |
| 17 | + | //! `crate::shell::site_header` puts Library, Dashboard and the cart behind | |
| 18 | + | //! `Some(user)`. A screen like that needs a per-request viewer whose user is | |
| 19 | + | //! optional, and `/team` is the smallest of them: static content, no writes, no | |
| 20 | + | //! reads, so the only thing being proved is the mount. | |
| 21 | + | //! | |
| 22 | + | //! # One behaviour this changes, and it is a fix | |
| 23 | + | //! | |
| 24 | + | //! `team_page` resolved its user through `MaybeUserUnverified`, which reads the | |
| 25 | + | //! session and short-circuits a legacy one but never validates the tracking row. | |
| 26 | + | //! [`super::viewer_factory`] calls `crate::auth::authenticate`, which does: | |
| 27 | + | //! a revoked or suspended session renders as signed out here where it would have | |
| 28 | + | //! kept rendering as signed in. That is the posture `auth`'s own doc recommends | |
| 29 | + | //! ("prefer this anywhere the identity actually gates"), and the cost is one | |
| 30 | + | //! cached session touch on a page that made no database call at all before. | |
| 31 | + | //! | |
| 32 | + | //! # The card is a row, and the bio is a disclosure | |
| 33 | + | //! | |
| 34 | + | //! `<details class="team-card-bio">` is the exact disclosure shape the wiki note | |
| 35 | + | //! records (`mnw-server-conversion-plan`, 2026-08-26): a selective region | |
| 36 | + | //! showing at most one, holding exactly one labelled sub-region. The near miss | |
| 37 | + | //! -- label on the outer slot -- renders as a carousel with Prev/Next buttons | |
| 38 | + | //! and compiles fine, so [`the_bio_is_a_disclosure_and_not_a_stepper`] asserts | |
| 39 | + | //! the rendering rather than the text. | |
| 40 | + | //! | |
| 41 | + | //! [`the_bio_is_a_disclosure_and_not_a_stepper`]: tests::the_bio_is_a_disclosure_and_not_a_stepper | |
| 42 | + | ||
| 43 | + | use makeover_layout as layout; | |
| 44 | + | use quasi_router::screen::{Act, Row}; | |
| 45 | + | use quasi_router::{ | |
| 46 | + | Action, Document, Node, RegionKind, Request, Response, RouteError, Screen as Described, Slot, | |
| 47 | + | }; | |
| 48 | + | use quasi_webview::Webview; | |
| 49 | + | ||
| 50 | + | /// The address, registered whole. See [`super::public_document_mount`]. | |
| 51 | + | pub const PATH: &str = "/team"; | |
| 52 | + | ||
| 53 | + | /// The page's own region, and what the skip link points at. | |
| 54 | + | pub const PAGE_REGION: &str = "team"; | |
| 55 | + | ||
| 56 | + | /// The disclosure that holds the founder's bio. | |
| 57 | + | const BIO: &str = "founder-bio"; | |
| 58 | + | ||
| 59 | + | /// The one labelled frame inside it, which is what makes it a disclosure rather | |
| 60 | + | /// than a one-slide carousel. | |
| 61 | + | const BIO_BODY: &str = "founder-bio-body"; | |
| 62 | + | ||
| 63 | + | const MEASURE: layout::Measure = layout::Measure::Wide; | |
| 64 | + | ||
| 65 | + | /// The page, which reads nothing and so takes no round trip. | |
| 66 | + | pub fn screen(_viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> { | |
| 67 | + | Ok(page_screen().into()) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | /// The whole document: the title, the measure, the body. | |
| 71 | + | fn page_screen() -> Described { | |
| 72 | + | Described::single("Team - Makenotwork") | |
| 73 | + | .measured(MEASURE) | |
| 74 | + | .documented(Document::default().classed(crate::shell::body_class(MEASURE, &["team-page"]))) | |
| 75 | + | .summarised("Small by design: who builds Makenotwork, and what they did before.") | |
| 76 | + | .with( | |
| 77 | + | Slot::new(PAGE_REGION, RegionKind::Pane) | |
| 78 | + | .with(Node::page("Team")) | |
| 79 | + | .with(Node::text( | |
| 80 | + | "Small by design. The base platform was designed by the founder, who intends \ | |
| 81 | + | to stay as the technical lead through the project's growth.", | |
| 82 | + | )) | |
| 83 | + | .with(Node::section("Founder")) | |
| 84 | + | .with(Node::list([founder()])) | |
| 85 | + | .with(bio()), | |
| 86 | + | ) | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | /// The one card on the page. | |
| 90 | + | /// | |
| 91 | + | /// A row rather than a grid of one: `use-case-grid-2col` held a single article, | |
| 92 | + | /// so the grid was describing an intention rather than a layout. When there is a | |
| 93 | + | /// second person the list grows a second row and the design system decides how | |
| 94 | + | /// two rows sit, which is the arrangement question answered once for every | |
| 95 | + | /// screen instead of in this page's own CSS. | |
| 96 | + | fn founder() -> Row { | |
| 97 | + | Row::new("Max") | |
| 98 | + | .secondary("CEO & Tech Lead") | |
| 99 | + | .meta("Designed and built the platform.") | |
| 100 | + | .act(Act::new("@max", Action::get("/u/max").navigating())) | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | /// The bio, closed until asked for. | |
| 104 | + | /// | |
| 105 | + | /// `showing_at_most_one(None)` is closed; `Some(0)` would be `<details open>`. | |
| 106 | + | fn bio() -> Node { | |
| 107 | + | Node::Region( | |
| 108 | + | Slot::new(BIO, RegionKind::Group) | |
| 109 | + | .with(Node::Region( | |
| 110 | + | Slot::new(BIO_BODY, RegionKind::Pane) | |
| 111 | + | .label("Bio") | |
| 112 | + | .with(Node::text(BIO_TEXT)), | |
| 113 | + | )) | |
| 114 | + | .showing_at_most_one(None), | |
| 115 | + | ) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | /// Carried over verbatim from the template. Prose the founder wrote, so it is | |
| 119 | + | /// content rather than copy and nothing here rewrites it. | |
| 120 | + | const BIO_TEXT: &str = "I started in the creator space as a young kid making backgrounds and \ | |
| 121 | + | montages for CoD MW2 players. I picked up programming around the same time, helping Minecraft \ | |
| 122 | + | server owners with whatever they needed. I spent the years after that focused on my \ | |
| 123 | + | education: a BA in mathematics at the University of Chicago, then a PhD in mathematics at UC \ | |
| 124 | + | San Diego. Afterward I decided to start my own business, and here we are."; | |
| 125 | + | ||
| 126 | + | /// The document this screen is drawn in. | |
| 127 | + | /// | |
| 128 | + | /// The head, the tail and the token meta come off | |
| 129 | + | /// [`super::Viewer::document_shell`]. What is added is what every public page on | |
| 130 | + | /// this site opens with: the skip link, and the site header, which reads the | |
| 131 | + | /// viewer's user through an `Option` because on this mount there may not be one. | |
| 132 | + | #[must_use] | |
| 133 | + | pub fn renderer(viewer: &super::Viewer) -> Webview { | |
| 134 | + | Webview::new().with_shell(viewer.document_shell().with_body_first(format!( | |
| 135 | + | "{}{}", | |
| 136 | + | crate::shell::skip_link(PAGE_REGION), | |
| 137 | + | crate::shell::site_header(viewer.user.as_ref(), Some(&viewer.csrf)), | |
| 138 | + | ))) | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | #[cfg(test)] | |
| 142 | + | mod tests { | |
| 143 | + | use super::*; | |
| 144 | + | ||
| 145 | + | fn html() -> String { | |
| 146 | + | use quasi_axum::Serves as _; | |
| 147 | + | ||
| 148 | + | Webview::new().screen(&page_screen()) | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | /// `2790e5c4` for this screen. The template wrote the measure on the body | |
| 152 | + | /// and `team-page` on a container div inside it; a described document has no | |
| 153 | + | /// container, so both land on the body and the CSS measure moved onto | |
| 154 | + | /// `.team-page main` the way `/feed`'s did. Same two classes, one element. | |
| 155 | + | #[test] | |
| 156 | + | fn the_document_carries_the_classes_the_template_carried() { | |
| 157 | + | let screen = page_screen(); | |
| 158 | + | ||
| 159 | + | assert_eq!( | |
| 160 | + | screen.document.body_class.as_deref(), | |
| 161 | + | Some("padded-page team-page") | |
| 162 | + | ); | |
| 163 | + | assert!(html().contains("padded-page team-page"), "{}", html()); | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | /// The disclosure shape, asserted by what it renders rather than by what it | |
| 167 | + | /// says: a label on the outer slot instead of the inner one is a legal | |
| 168 | + | /// carousel, so the text is identical and the markup is not. | |
| 169 | + | #[test] | |
| 170 | + | fn the_bio_is_a_disclosure_and_not_a_stepper() { | |
| 171 | + | let html = html(); | |
| 172 | + | ||
| 173 | + | assert!(html.contains(r#"aria-expanded="false""#), "{html}"); | |
| 174 | + | assert!(!html.contains(r#"data-shows="next""#), "{html}"); | |
| 175 | + | assert!(html.contains("University of Chicago"), "{html}"); | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | /// The founder's row says who, what, and where to read more. | |
| 179 | + | #[test] | |
| 180 | + | fn the_founder_row_links_to_the_profile() { | |
| 181 | + | let html = html(); | |
| 182 | + | ||
| 183 | + | assert!(html.contains("CEO & Tech Lead"), "{html}"); | |
| 184 | + | assert!(html.contains(r#"href="/u/max""#), "{html}"); | |
| 185 | + | } | |
| 186 | + | ||
| 187 | + | /// `736f45a5`: this screen's markup carries none of the four spellings. | |
| 188 | + | #[test] | |
| 189 | + | fn the_page_spells_no_spinner() { | |
| 190 | + | let html = html(); | |
| 191 | + | ||
| 192 | + | for spelling in ["htmx-indicator", "spinner", "loading-text", "loading-state"] { | |
| 193 | + | assert!(!html.contains(spelling), "{spelling} survives in {html}"); | |
| 194 | + | } | |
| 195 | + | } | |
| 196 | + | } |