Skip to main content

max / makenotwork

33.1 KB · 935 lines History Blame Raw
1 //! Templates for public-facing pages: landing, auth, content, blog, discover.
2 //!
3 //! Git source-browser templates live in the `git` submodule and are
4 //! re-exported flat, call sites still see `templates::GitRepoTemplate` etc.
5
6 mod git;
7 mod health;
8
9 pub use git::*;
10 pub use health::*;
11
12 use std::sync::Arc;
13
14 use askama::Template;
15
16 use crate::auth::SessionUser;
17 use crate::types::{
18 BlogPostSummary, Chapter, Collection, CollectionItem, CustomLink, DiscoverItem,
19 DiscoverProject, Item, ItemContent, ItemSection, Project, SidebarView, SubscriptionTier,
20 TagBreadcrumb, TagTreeNode, User, UserSubscription, Version, WaveStats,
21 };
22
23 use super::CsrfTokenOption;
24
25 // Public Pages
26
27 /// Sandbox info page explaining the ephemeral demo mode.
28 #[derive(Template)]
29 #[template(path = "pages/sandbox.html")]
30 pub struct SandboxTemplate {
31 pub csrf_token: CsrfTokenOption,
32 }
33
34 /// Content policy page.
35 #[derive(Template)]
36 #[template(path = "pages/policy.html")]
37 pub struct PolicyTemplate {
38 /// CSRF token injected into forms; `None` on public pages that have no forms.
39 pub csrf_token: CsrfTokenOption,
40 /// Logged-in user context for the site header; `None` when not authenticated.
41 pub session_user: Option<SessionUser>,
42 }
43
44 /// Landing page.
45 #[derive(Template)]
46 #[template(path = "pages/index.html")]
47 pub struct IndexTemplate {
48 pub csrf_token: CsrfTokenOption,
49 pub host_url: Arc<str>,
50 pub total_creators: u32,
51 pub total_items: u32,
52 /// Whether the founder pricing window is currently open. When true the
53 /// landing page features the founder rate prominently and links to /docs
54 /// /guide/tiers. See `project_founder_pricing.md`.
55 pub founder_window_open: bool,
56 /// Remaining founder slots (1,000 cap). Only shown when small enough to
57 /// convey urgency; not exposed when comfortably above the cap.
58 pub founder_slots_remaining: Option<u32>,
59 pub tier_prices: crate::tier_prices::TierPrices,
60 /// Screenshot frames for the "see the platform" carousel. Currently
61 /// placeholders; swap the images (and tighten the alt text) once real
62 /// captures exist. Empty hides the section.
63 pub landing_carousel: Vec<super::CarouselFrame>,
64 /// The "Last shipped" velocity line, drawn from the most recent published,
65 /// landing-flagged changelog post. `None` suppresses the line entirely
66 /// (no placeholder), same honesty rule the runway disclosure uses.
67 pub last_shipped: Option<LandingVelocity>,
68 }
69
70 /// One-line "Last shipped" velocity signal for the landing page.
71 pub struct LandingVelocity {
72 /// Post title.
73 pub title: String,
74 /// Publication date, preformatted (e.g. "Jun 07, 2026").
75 pub date: String,
76 /// Link target: `/changelog/{slug}`.
77 pub href: String,
78 }
79
80 /// User's library shell with inline purchases tab (other tabs loaded via HTMX).
81 #[derive(Template)]
82 #[template(path = "pages/library.html")]
83 pub struct LibraryTemplate {
84 pub csrf_token: CsrfTokenOption,
85 pub session_user: Option<SessionUser>,
86 pub purchases: Vec<crate::db::DbPurchaseRow>,
87 pub subscriptions: Vec<UserSubscription>,
88 pub has_mt_memberships: bool,
89 }
90
91 /// Shopping cart page with items grouped by seller.
92 #[derive(Template)]
93 #[template(path = "pages/cart.html")]
94 pub struct CartTemplate {
95 pub csrf_token: CsrfTokenOption,
96 pub session_user: Option<SessionUser>,
97 pub seller_groups: Vec<CartSellerGroup>,
98 pub wishlist_suggestions: Vec<crate::db::wishlists::WishlistItem>,
99 pub total_items: usize,
100 /// Set to "partial" when a multi-seller checkout partially succeeded.
101 pub checkout_status: String,
102 }
103
104 /// A group of cart items from the same seller.
105 pub struct CartSellerGroup {
106 pub seller_username: String,
107 pub seller_id: String,
108 pub stripe_ready: bool,
109 pub items: Vec<crate::db::cart::CartItem>,
110 /// i64 so a large cart can't overflow when per-item cents are summed (Pay-M3);
111 /// the per-item price is i32 but the running total is widened.
112 pub subtotal_cents: i64,
113 pub item_count: usize,
114 /// How much the creator saves vs. individual purchases ($0.30 per extra item).
115 pub savings_cents: i32,
116 }
117
118 impl CartSellerGroup {
119 pub fn subtotal_display(&self) -> String {
120 crate::formatting::format_revenue(self.subtotal_cents)
121 }
122
123 pub fn savings_display(&self) -> String {
124 crate::formatting::format_revenue(self.savings_cents as i64)
125 }
126 }
127
128 /// Login page.
129 #[derive(Template)]
130 #[template(path = "pages/login.html")]
131 pub struct LoginTemplate {
132 pub csrf_token: CsrfTokenOption,
133 /// Re-displayed in the username/email input on validation failure so the
134 /// user doesn't have to retype it. Empty on the first GET.
135 pub prefill_login: String,
136 /// Shown inline above the form on a failed POST. None hides the banner.
137 pub error: Option<String>,
138 /// Neutral informational notice above the form (e.g. the access-gate prompt
139 /// on the testnot staging mirror). None hides it. Separate from `error` so
140 /// it doesn't render as a failure.
141 pub notice: Option<String>,
142 /// When true, the page shows a single "Sign in with Makenot.work" button
143 /// (delegated SSO) instead of the local password form. Set on the testnot
144 /// mirror where `[sso]` is configured.
145 pub sso_enabled: bool,
146 }
147
148 // Join Wizard
149
150 /// Full page: join/signup wizard.
151 #[derive(Template)]
152 #[template(path = "wizards/wizard_join.html")]
153 pub struct WizardJoinTemplate {
154 pub csrf_token: CsrfTokenOption,
155 pub nav: Vec<super::StepNavItem>,
156 pub invite_code: Option<String>,
157 /// Preserved input + error on a non-HTMX account-step re-render (so a
158 /// JS-disabled submit that fails validation doesn't drop the typed
159 /// username/email). Empty / `None` on the initial render.
160 pub username: String,
161 pub email: String,
162 pub error: Option<String>,
163 pub error_field: Option<String>,
164 }
165
166 /// Step 1 partial: account creation (back-nav reload, and the HTMX validation
167 /// re-render). Carries preserved input + error so a failed HTMX submit swaps the
168 /// form back with the typed username/email intact and the bad field flagged,
169 /// instead of replacing the whole step with a bare error line (UX-S1, Run #23).
170 #[derive(Template)]
171 #[template(path = "wizards/steps/join/account.html")]
172 pub struct WizardJoinAccountTemplate {
173 pub nav: Vec<super::StepNavItem>,
174 pub csrf_token: CsrfTokenOption,
175 pub invite_code: Option<String>,
176 pub username: String,
177 pub email: String,
178 pub error: Option<String>,
179 pub error_field: Option<String>,
180 }
181
182 /// Step 2 partial: profile (display name + bio).
183 #[derive(Template)]
184 #[template(path = "wizards/steps/join/profile.html")]
185 pub struct WizardJoinProfileTemplate {
186 pub nav: Vec<super::StepNavItem>,
187 }
188
189 /// Step 3 partial: welcome/complete with intent branching.
190 #[derive(Template)]
191 #[template(path = "wizards/steps/join/complete.html")]
192 pub struct WizardJoinCompleteTemplate {
193 pub nav: Vec<super::StepNavItem>,
194 pub display_name: String,
195 /// Whether this user already has creator access.
196 pub is_creator: bool,
197 /// Whether this user arrived via invite (already has waitlist entry).
198 pub has_invite: bool,
199 }
200
201 /// Two-factor authentication verification page (login flow).
202 #[derive(Template)]
203 #[template(path = "pages/two_factor.html")]
204 pub struct TwoFactorTemplate {
205 pub csrf_token: CsrfTokenOption,
206 pub session_user: Option<SessionUser>,
207 pub error: Option<String>,
208 }
209
210 /// OAuth2 authorization / consent page.
211 #[derive(Template)]
212 #[template(path = "pages/oauth_authorize.html")]
213 pub struct OAuthAuthorizeTemplate {
214 pub csrf_token: CsrfTokenOption,
215 pub session_user: Option<SessionUser>,
216 pub app_name: String,
217 pub client_id: String,
218 pub redirect_uri: String,
219 pub state: String,
220 pub code_challenge: String,
221 pub code_challenge_method: String,
222 /// Space-delimited requested scope, round-tripped through the consent form
223 /// so the granted scope survives the GET -> POST hop.
224 pub scope: String,
225 pub error_message: Option<String>,
226 }
227
228 /// Forgot password form.
229 #[derive(Template)]
230 #[template(path = "pages/forgot_password.html")]
231 pub struct ForgotPasswordTemplate {
232 pub csrf_token: CsrfTokenOption,
233 }
234
235 /// Password reset form (reached via email link).
236 #[derive(Template)]
237 #[template(path = "pages/reset_password.html")]
238 pub struct ResetPasswordTemplate {
239 pub csrf_token: CsrfTokenOption,
240 pub valid: bool,
241 /// The single-use reset token, round-tripped through the form's hidden
242 /// field so the POST can consume it. Empty when the link is invalid.
243 pub token: String,
244 /// Inline error banner (e.g. "Passwords do not match"). None hides the
245 /// banner. Used on non-HTMX form-validation failures so the user stays
246 /// on the form with the token field intact.
247 pub error: Option<String>,
248 }
249
250 /// Public user profile page.
251 #[derive(Template)]
252 #[template(path = "pages/user.html")]
253 #[allow(dead_code)] // Fields used by Askama template
254 pub struct UserTemplate {
255 pub csrf_token: CsrfTokenOption,
256 pub session_user: Option<SessionUser>,
257 pub user: User,
258 pub custom_links: Vec<CustomLink>,
259 pub projects: Vec<Project>,
260 pub public_collections: Vec<Collection>,
261 /// User ID for the follow button target.
262 pub user_id: String,
263 /// Whether the current viewer is looking at their own profile.
264 pub is_own_profile: bool,
265 /// Whether the current viewer is following this user.
266 pub is_following: bool,
267 /// Total follower count for this user.
268 pub follower_count: i64,
269 /// Base URL for OG meta tags.
270 pub host_url: Arc<str>,
271 /// Whether this creator has voluntarily paused their account.
272 pub creator_paused: bool,
273 /// Whether this creator accepts tips.
274 pub tips_enabled: bool,
275 /// Creator's user ID for tip checkout (string for template use).
276 pub creator_id: String,
277 /// Project ID for tip attribution (None on user profile pages).
278 pub tip_project_id: Option<String>,
279 /// Pre-rendered primitive-layer CSS for this creator's chosen theme,
280 /// injected into the page `<head>` (Tier 0). See `crate::theming`.
281 pub theme_css: &'static str,
282 }
283
284 /// Public collection page (shareable URL).
285 #[derive(Template)]
286 #[template(path = "pages/collection.html")]
287 #[allow(dead_code)] // Fields used by Askama template
288 pub struct CollectionTemplate {
289 pub csrf_token: CsrfTokenOption,
290 pub session_user: Option<SessionUser>,
291 pub collection: Collection,
292 pub items: Vec<CollectionItem>,
293 pub owner_username: String,
294 pub owner_display_name: Option<String>,
295 pub is_owner: bool,
296 }
297
298 /// Public project page with item listing.
299 #[derive(Template)]
300 #[template(path = "pages/project.html")]
301 #[allow(dead_code)] // Fields used by Askama template
302 pub struct ProjectTemplate {
303 pub csrf_token: CsrfTokenOption,
304 pub session_user: Option<SessionUser>,
305 pub project: Project,
306 pub creator_username: String,
307 pub items: Vec<Item>,
308 /// Project ID for the follow button target.
309 pub project_id: String,
310 /// Whether the current viewer is following this project.
311 pub is_following: bool,
312 /// Total follower count for this project.
313 pub follower_count: i64,
314 /// Active subscription tiers available for this project.
315 pub subscription_tiers: Vec<SubscriptionTier>,
316 /// Whether the current viewer already has an active subscription.
317 pub has_subscription: bool,
318 /// Base URL for OG meta tags.
319 pub host_url: Arc<str>,
320 /// Linked git repositories: (name, URL) pairs.
321 pub git_repos: Vec<(String, String)>,
322 /// Whether this project has any published blog posts.
323 pub has_blog_posts: bool,
324 /// URL to the paired MT community forum (None if no community provisioned).
325 pub community_url: Option<String>,
326 /// Whether the project owner accepts tips.
327 pub tips_enabled: bool,
328 /// Creator's user ID for tip checkout (string for template use).
329 pub creator_id: String,
330 /// Project ID for tip attribution.
331 pub tip_project_id: Option<String>,
332 /// Whether the current viewer owns this project.
333 pub is_owner: bool,
334 /// Tabbed markdown sections (privacy, terms, FAQ, etc).
335 pub sections: Vec<crate::types::ProjectSection>,
336 /// Ordered gallery images rendered through the shared carousel widget
337 /// (empty → the carousel section is suppressed). Additive to cover_image_url.
338 pub gallery: Vec<super::CarouselFrame>,
339 /// Pre-rendered primitive-layer CSS for this project's chosen theme,
340 /// injected into the page `<head>` (Tier 0). See `crate::theming`.
341 pub theme_css: &'static str,
342 }
343
344 /// Project paywall landing page (shown when a project requires purchase/subscription).
345 #[derive(Template)]
346 #[template(path = "pages/project_paywall.html")]
347 pub struct ProjectPaywallTemplate {
348 pub csrf_token: CsrfTokenOption,
349 pub session_user: Option<SessionUser>,
350 pub project: Project,
351 pub creator_username: String,
352 /// Human-readable pricing (e.g. "$19.99", "Subscription").
353 pub price_display: String,
354 /// What kind of checkout flow is needed.
355 pub checkout_type: crate::pricing::CheckoutType,
356 /// Available subscription tiers (for subscription-model projects).
357 pub subscription_tiers: Vec<SubscriptionTier>,
358 /// Base URL for OG meta tags.
359 pub host_url: Arc<str>,
360 }
361
362 /// Public item detail page.
363 #[derive(Template)]
364 #[template(path = "pages/item.html")]
365 #[allow(dead_code)] // Fields used by Askama template
366 pub struct ItemTemplate {
367 pub csrf_token: CsrfTokenOption,
368 pub session_user: Option<SessionUser>,
369 pub item: Item,
370 pub creator_username: String,
371 pub project_title: String,
372 pub project_slug: String,
373 /// Base URL for OG meta tags.
374 pub host_url: Arc<str>,
375 /// URL to the MT discussion thread (None if no linked thread or MT unavailable).
376 pub discussion_url: Option<String>,
377 /// Number of posts in the linked discussion thread.
378 pub discussion_count: Option<i64>,
379 /// Project cover image URL (fallback for og:image when item has no cover).
380 pub project_cover_image_url: Option<String>,
381 /// Child items for bundle-type items (empty for non-bundles).
382 pub bundle_items: Vec<Item>,
383 /// Bundles containing this item (for unlisted items, to show "Available in" links).
384 pub containing_bundles: Vec<Item>,
385 /// Tabbed content sections (e.g. Features, Installation, Specs).
386 pub sections: Vec<ItemSection>,
387 /// Whether the current user is the item's creator (for dashboard links).
388 pub is_owner: bool,
389 /// Whether the current user has wishlisted this item.
390 pub is_wishlisted: bool,
391 /// Whether the current user has this item in their cart.
392 pub in_cart: bool,
393 /// How many of the current user's collections contain this item.
394 pub collection_count: u32,
395 /// Whether the current user can consume this item (purchased, free, subscribed, creator, bundle).
396 /// Drives the store-page CTA swap: true → "View in library", false → Buy/PWYW.
397 pub has_access: bool,
398 /// Ordered gallery images rendered through the shared carousel widget
399 /// (empty → the carousel section is suppressed). Additive to cover_image_url.
400 pub gallery: Vec<super::CarouselFrame>,
401 /// Pre-rendered primitive-layer CSS for the parent project's chosen theme
402 /// (items inherit it), injected into `<head>` (Tier 0). See `crate::theming`.
403 pub theme_css: &'static str,
404 }
405
406 /// Library (consumption) view for download / bundle / other items.
407 /// Audio + video items currently render this too; dedicated templates land in
408 /// Phases 2–3.
409 #[derive(Template)]
410 #[template(path = "pages/library_downloads.html")]
411 #[allow(dead_code)]
412 pub struct LibraryDownloadsTemplate {
413 pub csrf_token: CsrfTokenOption,
414 pub session_user: Option<SessionUser>,
415 pub item: Item,
416 pub creator_username: String,
417 pub project_title: String,
418 pub project_slug: String,
419 pub host_url: Arc<str>,
420 pub versions: Vec<Version>,
421 /// Child items if this is a bundle; otherwise empty. Children get `/l/` links
422 /// because the viewer (by being on this page) has access via the bundle.
423 pub bundle_items: Vec<Item>,
424 pub sections: Vec<ItemSection>,
425 pub discussion_url: Option<String>,
426 pub discussion_count: Option<i64>,
427 pub is_owner: bool,
428 }
429
430 /// 403 page shown when a viewer hits /l/{id} but lacks access.
431 #[derive(Template)]
432 #[template(path = "pages/library_locked.html")]
433 #[allow(dead_code)]
434 pub struct LibraryLockedTemplate {
435 pub csrf_token: CsrfTokenOption,
436 pub session_user: Option<SessionUser>,
437 pub item: Item,
438 pub creator_username: String,
439 pub host_url: Arc<str>,
440 /// For unlisted items: bundles that contain this item.
441 pub containing_bundles: Vec<Item>,
442 pub is_logged_in: bool,
443 }
444
445 /// Library (consumption) view for text items, full article body, discussion.
446 #[derive(Template)]
447 #[template(path = "pages/library_text.html")]
448 #[allow(dead_code)]
449 pub struct LibraryTextTemplate {
450 pub csrf_token: CsrfTokenOption,
451 pub session_user: Option<SessionUser>,
452 pub item: Item,
453 pub creator_username: String,
454 pub creator_display_name: Option<String>,
455 pub creator_avatar_initials: String,
456 pub project_title: String,
457 pub project_slug: String,
458 /// Fully rendered article body HTML.
459 pub body_html: Option<String>,
460 pub reading_time: Option<String>,
461 pub host_url: Arc<str>,
462 pub discussion_url: Option<String>,
463 pub discussion_count: Option<i64>,
464 pub is_owner: bool,
465 }
466
467 /// Blog/article reader view.
468 #[derive(Template)]
469 #[template(path = "pages/text_reader.html")]
470 #[allow(dead_code)] // Fields used by Askama template
471 pub struct TextReaderTemplate {
472 pub csrf_token: CsrfTokenOption,
473 pub session_user: Option<SessionUser>,
474 pub item: Item,
475 pub creator_username: String,
476 pub creator_display_name: Option<String>,
477 /// First-letter initials for the avatar circle (e.g. "JD" for "Jane Doe").
478 pub creator_avatar_initials: String,
479 pub project_title: String,
480 pub project_slug: String,
481 /// Whether the item has a zero price (free content, no purchase required).
482 pub is_free: bool,
483 /// Whether the current user already has this item in their library.
484 pub in_library: bool,
485 /// Drives the CTA swap: true → "Read in library", false → Buy/PWYW/Add-to-Library.
486 pub has_access: bool,
487 pub reading_time: Option<String>,
488 /// Short plain-text preview of the article body, shown on the store page.
489 pub excerpt: Option<String>,
490 /// Base URL for OG meta tags.
491 pub host_url: Arc<str>,
492 /// URL to the MT discussion thread (None if no linked thread or MT unavailable).
493 pub discussion_url: Option<String>,
494 /// Number of posts in the linked discussion thread.
495 pub discussion_count: Option<i64>,
496 }
497
498 /// Library (consumption) view for audio items, full player, chapters,
499 /// description, optional source-file downloads, discussion.
500 #[derive(Template)]
501 #[template(path = "pages/library_audio.html")]
502 #[allow(dead_code)]
503 pub struct LibraryAudioTemplate {
504 pub csrf_token: CsrfTokenOption,
505 pub session_user: Option<SessionUser>,
506 pub item: Item,
507 pub creator_username: String,
508 pub creator_display_name: Option<String>,
509 pub creator_avatar_initials: String,
510 pub project_title: Option<String>,
511 pub project_slug: String,
512 pub audio_url: Option<String>,
513 pub chapters: Vec<Chapter>,
514 pub segments_json: String,
515 /// Source-file downloads if the creator offers them alongside the stream.
516 pub versions: Vec<Version>,
517 pub host_url: Arc<str>,
518 pub discussion_url: Option<String>,
519 pub discussion_count: Option<i64>,
520 pub is_owner: bool,
521 }
522
523 /// Audio streaming player view.
524 #[derive(Template)]
525 #[template(path = "pages/audio_player.html")]
526 pub struct AudioPlayerTemplate {
527 pub csrf_token: CsrfTokenOption,
528 pub session_user: Option<SessionUser>,
529 pub item: Item,
530 pub creator_username: String,
531 pub creator_display_name: Option<String>,
532 /// First-letter initials for the avatar circle.
533 pub creator_avatar_initials: String,
534 pub project_title: Option<String>,
535 pub project_slug: String,
536 /// Whether the item has a zero price.
537 pub is_free: bool,
538 /// Whether the current user already has this item in their library.
539 pub in_library: bool,
540 /// Drives the CTA swap: true → "View in library", false → Buy/PWYW or Add-to-Library.
541 pub has_access: bool,
542 /// Base URL for OG meta tags.
543 pub host_url: Arc<str>,
544 /// URL to the MT discussion thread (None if no linked thread or MT unavailable).
545 pub discussion_url: Option<String>,
546 /// Number of posts in the linked discussion thread.
547 pub discussion_count: Option<i64>,
548 }
549
550 /// Library (consumption) view for video items, full player, chapters,
551 /// description, optional source-file downloads, discussion.
552 #[derive(Template)]
553 #[template(path = "pages/library_video.html")]
554 #[allow(dead_code)]
555 pub struct LibraryVideoTemplate {
556 pub csrf_token: CsrfTokenOption,
557 pub session_user: Option<SessionUser>,
558 pub item: Item,
559 pub creator_username: String,
560 pub creator_display_name: Option<String>,
561 pub creator_avatar_initials: String,
562 pub project_title: Option<String>,
563 pub project_slug: String,
564 pub video_url: Option<String>,
565 pub chapters: Vec<Chapter>,
566 pub segments_json: String,
567 pub versions: Vec<Version>,
568 pub host_url: Arc<str>,
569 pub discussion_url: Option<String>,
570 pub discussion_count: Option<i64>,
571 pub is_owner: bool,
572 }
573
574 /// Video player page with custom controls, insertions, chapters.
575 #[derive(Template)]
576 #[template(path = "pages/video_player.html")]
577 pub struct VideoPlayerTemplate {
578 pub csrf_token: CsrfTokenOption,
579 pub session_user: Option<SessionUser>,
580 pub item: Item,
581 pub creator_username: String,
582 pub creator_display_name: Option<String>,
583 pub creator_avatar_initials: String,
584 pub project_title: Option<String>,
585 pub project_slug: String,
586 pub is_free: bool,
587 pub in_library: bool,
588 pub has_access: bool,
589 pub host_url: Arc<str>,
590 pub discussion_url: Option<String>,
591 pub discussion_count: Option<i64>,
592 }
593
594 /// Browse/discover page with filtering and pagination.
595 #[derive(Template)]
596 #[template(path = "pages/discover.html")]
597 pub struct DiscoverTemplate {
598 pub csrf_token: CsrfTokenOption,
599 pub session_user: Option<SessionUser>,
600 pub items: Vec<DiscoverItem>,
601 pub projects: Vec<DiscoverProject>,
602 /// Active browse mode: `"items"` or `"projects"`.
603 pub mode: String,
604 pub total_items: u32,
605 pub current_page: u32,
606 pub total_pages: u32,
607 pub search_query: String,
608 /// A search term was applied. Drives the empty-state copy.
609 ///
610 /// Not `!search_query.is_empty()`: that is the raw `?q=`, and a
611 /// whitespace-only term is browsing as far as the query is concerned.
612 pub is_search: bool,
613 /// The rendered count line ("247 results", "1 item"). Built once in
614 /// `results_count_label` because the page and the out-of-band partial both
615 /// render `#total-count`; a difference between them would show up as the
616 /// text changing on the first HTMX swap.
617 pub count_label: String,
618 /// Active sort key (e.g. `"most_sold"`, `"newest"`, `"price_asc"`).
619 pub sort_by: String,
620 /// Page numbers to render in the pagination bar.
621 pub pagination_range: Vec<u32>,
622 pub showing_start: u32,
623 pub showing_end: u32,
624 /// Everything the sidebar renders. Grouped so the results partial can carry
625 /// the identical set for its out-of-band swap.
626 pub sidebar: SidebarView,
627 /// Whether the current user is authenticated (for collection save buttons in results).
628 pub is_authenticated: bool,
629 /// Always false here: the page renders the sidebar directly, so the
630 /// included results partial must not emit a second out-of-band copy.
631 pub oob_sidebar: bool,
632 }
633
634 /// Tag tree browser with breadcrumb navigation.
635 #[derive(Template)]
636 #[template(path = "pages/tag_tree.html")]
637 pub struct TagTreeTemplate {
638 pub csrf_token: CsrfTokenOption,
639 pub session_user: Option<SessionUser>,
640 pub categories: Vec<TagTreeNode>,
641 pub breadcrumbs: Vec<TagBreadcrumb>,
642 pub current_tag: Option<TagBreadcrumb>,
643 }
644
645 /// Purchase confirmation page showing fee breakdown.
646 #[derive(Template)]
647 #[template(path = "pages/purchase.html")]
648 pub struct PurchaseTemplate {
649 pub csrf_token: CsrfTokenOption,
650 pub item: Item,
651 pub creator_username: String,
652 pub stripe_fee: String,
653 pub creator_receives: String,
654 /// Pre-filled promo code from `?code=` query parameter.
655 pub promo_code: String,
656 /// Whether PWYW pricing is enabled for this item.
657 pub pwyw_enabled: bool,
658 /// Minimum price in cents when PWYW is enabled.
659 pub pwyw_min_cents: i32,
660 /// Formatted suggested price in dollars (e.g. "9.99").
661 pub suggested_price: String,
662 /// Formatted minimum price in dollars (e.g. "1.00").
663 pub pwyw_min_dollars: String,
664 /// Whether the creator has Stripe Tax enabled.
665 pub stripe_tax_enabled: bool,
666 /// Whether the current visitor is logged in (show guest checkout if not).
667 pub is_logged_in: bool,
668 /// If the buyer has an in-progress (pending) checkout for this item,
669 /// the relative time it was started (e.g. "5 minutes ago"). Empty
670 /// string means no pending checkout.
671 pub pending_started: String,
672 }
673
674 /// Minimal direct purchase page, no navigation, for link-in-bio sharing.
675 #[derive(Template)]
676 #[template(path = "pages/buy.html")]
677 pub struct BuyPageTemplate {
678 pub item: Item,
679 pub creator_username: String,
680 pub creator_display_name: Option<String>,
681 pub pwyw_enabled: bool,
682 pub pwyw_min_dollars: String,
683 pub suggested_price: String,
684 pub host_url: Arc<str>,
685 }
686
687 /// Feed page showing items from followed users, projects, and tags.
688 #[derive(Template)]
689 #[template(path = "pages/feed.html")]
690 pub struct FeedTemplate {
691 pub csrf_token: CsrfTokenOption,
692 pub session_user: Option<SessionUser>,
693 pub items: Vec<DiscoverItem>,
694 pub total_items: u32,
695 pub current_page: u32,
696 pub total_pages: u32,
697 pub pagination_range: Vec<u32>,
698 pub showing_start: u32,
699 pub showing_end: u32,
700 }
701
702 /// Public page: Stripe Connect disclaimer and terms before onboarding.
703 #[derive(Template)]
704 #[template(path = "pages/stripe_disclaimer.html")]
705 pub struct StripeConnectDisclaimerTemplate {
706 pub csrf_token: CsrfTokenOption,
707 }
708
709 // Fan+
710
711 /// Fan+ subscription page: marketing, subscribe, or manage.
712 #[derive(Template)]
713 #[template(path = "pages/fan_plus.html")]
714 pub struct FanPlusTemplate {
715 pub csrf_token: CsrfTokenOption,
716 pub session_user: Option<SessionUser>,
717 /// Whether the user has an active Fan+ subscription.
718 pub is_subscribed: bool,
719 /// Current billing period end (if subscribed).
720 pub period_end: Option<String>,
721 /// Whether a `?subscribed=true` query was present (just subscribed).
722 pub just_subscribed: bool,
723 }
724
725 // Blog Pages
726
727 /// Public blog index for a project.
728 #[derive(Template)]
729 #[template(path = "pages/project_blog.html")]
730 pub struct ProjectBlogTemplate {
731 pub csrf_token: CsrfTokenOption,
732 pub session_user: Option<SessionUser>,
733 pub project: Project,
734 pub creator_username: String,
735 pub project_slug: String,
736 pub posts: Vec<BlogPostSummary>,
737 }
738
739 /// Public blog post reader.
740 #[derive(Template)]
741 #[template(path = "pages/blog_post.html")]
742 pub struct BlogPostTemplate {
743 pub csrf_token: CsrfTokenOption,
744 pub session_user: Option<SessionUser>,
745 pub title: String,
746 /// Title escaped for JSON string embedding (JSON-LD).
747 pub title_json: String,
748 pub body_html: String,
749 pub published_at: String,
750 pub creator_username: String,
751 pub creator_display_name: Option<String>,
752 pub creator_avatar_initials: String,
753 pub project_title: String,
754 /// Project title escaped for JSON string embedding (JSON-LD).
755 pub project_title_json: String,
756 pub project_slug: String,
757 /// URL-safe slug for this blog post.
758 pub post_slug: String,
759 /// Base URL for OG meta tags.
760 pub host_url: Arc<str>,
761 /// Project cover image URL (fallback for og:image when blog post has no specific image).
762 pub project_cover_image_url: Option<String>,
763 /// URL to the MT discussion thread (None if no linked thread or MT unavailable).
764 pub discussion_url: Option<String>,
765 /// Number of posts in the linked discussion thread.
766 pub discussion_count: Option<i64>,
767 }
768
769 // Documentation Pages
770
771 /// Individual documentation page.
772 #[derive(Template)]
773 #[template(path = "pages/doc.html")]
774 pub struct DocTemplate {
775 pub csrf_token: CsrfTokenOption,
776 pub session_user: Option<SessionUser>,
777 pub title: String,
778 pub section: String,
779 pub content: String,
780 /// Pages that link to this one ("what links here"), in docs-index order.
781 /// Empty when nothing links here, in which case the template omits the
782 /// section entirely.
783 pub backlinks: Vec<DocSectionEntry>,
784 }
785
786 /// Entry in a doc section for the index page.
787 pub struct DocSectionEntry {
788 pub title: String,
789 pub slug: String,
790 }
791
792 /// A collapsible subcategory within a doc section.
793 pub struct DocSubsection {
794 pub label: String,
795 pub entries: Vec<DocSectionEntry>,
796 }
797
798 /// A group of doc entries under a section heading.
799 pub struct DocSection {
800 pub name: String,
801 pub entries: Vec<DocSectionEntry>,
802 pub subsections: Vec<DocSubsection>,
803 }
804
805 /// Documentation index page listing all docs by section.
806 #[derive(Template)]
807 #[template(path = "pages/doc_index.html")]
808 pub struct DocIndexTemplate {
809 pub csrf_token: CsrfTokenOption,
810 pub session_user: Option<SessionUser>,
811 pub sections: Vec<DocSection>,
812 }
813
814 // Pricing Calculator
815
816 /// Interactive pricing calculator comparing MNW to competitors.
817 #[derive(Template)]
818 #[template(path = "pages/pricing.html")]
819 pub struct PricingTemplate {
820 pub csrf_token: CsrfTokenOption,
821 pub tier_prices: crate::tier_prices::TierPrices,
822 /// Server-computed initial comparison (default revenue + Basic tier). The
823 /// page re-fetches this partial via HTMX as the inputs change.
824 pub comparison: crate::pricing_comparison::ComparisonResult,
825 }
826
827 /// HTMX partial: the recomputed pricing comparison (MNW summary + competitor
828 /// table + breakeven note), swapped into `/pricing` on input change.
829 #[derive(Template)]
830 #[template(path = "partials/pricing_comparison.html")]
831 pub struct PricingComparisonPartial {
832 pub comparison: crate::pricing_comparison::ComparisonResult,
833 }
834
835 /// Platform economics + runway disclosure page. Renders at `/economics`
836 /// (the retired markdown page's `/docs/economics` URL 301s here). See the
837 /// doc comment on `templates/pages/economics.html` for the maintenance
838 /// contract.
839 #[derive(Template)]
840 #[template(path = "pages/economics.html")]
841 #[allow(dead_code)] // Fields used by Askama template
842 pub struct EconomicsTemplate {
843 pub csrf_token: CsrfTokenOption,
844 pub session_user: Option<SessionUser>,
845 /// `quarters` + `last_updated_iso` from `[runway]` in
846 /// `assumptions.toml`. Operator-edited; refreshed quarterly.
847 pub runway_config: crate::tier_prices::RunwayConfig,
848 /// Live count of `status='active'` creator subscriptions. Pulled at
849 /// request time so the page never lies about how many seats are
850 /// revenue-bearing right now.
851 pub paying_creators: i64,
852 /// Live count of `status='trialing'` plus canceled-with-grace
853 /// creators. Disclosed only when non-zero (the template hides the
854 /// bullet otherwise so a quiet platform doesn't show "0 in trial").
855 pub trialing_or_grace: i64,
856 }
857
858 /// Use cases page showcasing creator types.
859 #[derive(Template)]
860 #[template(path = "pages/use_cases.html")]
861 pub struct UseCasesTemplate {
862 pub csrf_token: CsrfTokenOption,
863 pub session_user: Option<SessionUser>,
864 pub tier_prices: crate::tier_prices::TierPrices,
865 }
866
867 /// Team page listing the founder, residents, and fellows.
868 #[derive(Template)]
869 #[template(path = "pages/team.html")]
870 pub struct TeamTemplate {
871 pub csrf_token: CsrfTokenOption,
872 pub session_user: Option<SessionUser>,
873 }
874
875 // Creator Invite System
876
877 /// Public page: creator invite waves and waitlist status.
878 #[derive(Template)]
879 #[template(path = "pages/creators.html")]
880 pub struct CreatorsTemplate {
881 pub csrf_token: CsrfTokenOption,
882 pub session_user: Option<SessionUser>,
883 pub waves: Vec<WaveStats>,
884 pub total_creators: u32,
885 pub waitlist_pending: u32,
886 pub is_creator: bool,
887 pub tier_prices: crate::tier_prices::TierPrices,
888 }
889
890 // Email & Account
891
892 /// Public page: email action result (verification, unsubscribe, etc.).
893 #[derive(Template)]
894 #[template(path = "pages/email_result.html")]
895 pub struct EmailResultTemplate {
896 pub csrf_token: CsrfTokenOption,
897 pub title: String,
898 pub message: String,
899 pub link_url: String,
900 pub link_text: String,
901 }
902
903 /// Purchase receipt page.
904 #[derive(Template)]
905 #[template(path = "pages/receipt.html")]
906 pub struct ReceiptTemplate {
907 pub csrf_token: CsrfTokenOption,
908 pub session_user: Option<crate::auth::SessionUser>,
909 pub transaction_id: String,
910 pub item_id: String,
911 pub item_title: String,
912 pub seller_username: String,
913 pub amount: String,
914 pub is_free: bool,
915 pub status: String,
916 pub date: String,
917 }
918
919 /// Confirmation page shown before account deletion (GET step).
920 #[derive(Template)]
921 #[template(path = "pages/confirm_delete.html")]
922 pub struct ConfirmDeleteTemplate {
923 pub csrf_token: CsrfTokenOption,
924 pub user: String,
925 pub expires: String,
926 pub sig: String,
927 }
928
929 /// Public page: confirmation that account has been deleted.
930 #[derive(Template)]
931 #[template(path = "pages/account-deleted.html")]
932 pub struct AccountDeletedTemplate {
933 pub csrf_token: CsrfTokenOption,
934 }
935