Skip to main content

max / makenotwork

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