Skip to main content

max / makenotwork

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