Skip to main content

max / makenotwork

33.9 KB · 1039 lines History Blame Raw
1 //! Templates for HTMX partials: alerts, form status, tab content, tags.
2
3 use std::sync::Arc;
4
5 use askama::Template;
6
7 use crate::db::{DbUserSession, UserSessionId};
8 use crate::types::{
9 AdminAppealRow, AdminHeldUploadRow, AdminReportRow, AdminUserRow, AdminWaitlistRow,
10 BlogPostDashboardRow, BuyerContact, ChartBar, Collection, ContactRow, ContentItem,
11 DiscoverItem, DiscoverProject, InviteCodeDisplay, Item, ItemSection, LicenseKeyRow,
12 PayoutSummary, Project, ProjectCard, ProjectComparison, ProjectMemberRow, PromoCodeRow,
13 SaleRow, SidebarView, StatCard, SubscriptionTier, SyncAppRow, TipReceived, Transaction, User,
14 UserSubscription, Version, WaitlistEntry, WaveStats,
15 };
16
17 use super::CsrfTokenOption;
18
19 // HTMX Partials
20
21 /// HTMX partial for discover page results.
22 #[derive(Template)]
23 #[template(path = "partials/discover_results.html")]
24 pub struct DiscoverResultsTemplate {
25 pub items: Vec<DiscoverItem>,
26 pub projects: Vec<DiscoverProject>,
27 pub mode: String,
28 pub total_items: u32,
29 pub current_page: u32,
30 pub total_pages: u32,
31 pub pagination_range: Vec<u32>,
32 pub showing_start: u32,
33 pub showing_end: u32,
34 /// Currently selected category slug, for HTMX pagination to preserve.
35 pub current_category: String,
36 /// A search term was applied. Drives the empty-state copy.
37 ///
38 /// Not `!search_query.is_empty()`: that is the raw `?q=`, and a
39 /// whitespace-only term is browsing as far as the query is concerned.
40 pub is_search: bool,
41 /// The rendered count line ("247 results", "1 item"). Built once in
42 /// `results_count_label` because the page and the out-of-band partial both
43 /// render `#total-count`; a difference between them would show up as the
44 /// text changing on the first HTMX swap.
45 pub count_label: String,
46 /// Whether the current user is authenticated (for collection save buttons).
47 pub is_authenticated: bool,
48 /// The sidebar, re-rendered for an out-of-band swap. Its counts describe the
49 /// filter that produced these results, so leaving the previous sidebar in
50 /// place would show numbers that disagree with the list beside them.
51 pub sidebar: SidebarView,
52 /// Emit the sidebar as an out-of-band swap. False when the full page
53 /// includes this partial, which renders the sidebar itself; true only for
54 /// the standalone `/discover/results` response.
55 pub oob_sidebar: bool,
56 }
57
58 /// HTMX partial: dismissible alert/notification banner.
59 #[derive(Template)]
60 #[template(path = "partials/alert.html")]
61 pub struct AlertTemplate {
62 pub alert_type: String,
63 pub message: String,
64 pub link_url: Option<String>,
65 pub link_text: Option<String>,
66 }
67
68 impl AlertTemplate {
69 pub fn new(alert_type: &str, message: &str) -> Self {
70 Self {
71 alert_type: alert_type.to_string(),
72 message: message.to_string(),
73 link_url: None,
74 link_text: None,
75 }
76 }
77
78 #[must_use]
79 pub fn with_link(mut self, url: &str, text: &str) -> Self {
80 self.link_url = Some(url.to_string());
81 self.link_text = Some(text.to_string());
82 self
83 }
84 }
85
86 /// HTMX partial: success/error status message after form submission.
87 #[derive(Template)]
88 #[template(path = "partials/form_status.html")]
89 pub struct FormStatusTemplate {
90 pub success: bool,
91 pub message: String,
92 }
93
94 impl FormStatusTemplate {
95 pub fn render_string(&self) -> crate::error::Result<String> {
96 crate::helpers::render_fragment(self)
97 }
98 }
99
100 /// HTMX partial: library action status message.
101 #[derive(Template)]
102 #[template(path = "partials/library_status.html")]
103 pub struct LibraryStatusTemplate {
104 pub message: String,
105 }
106
107 /// HTMX partial: data-URI download link for an export file.
108 #[derive(Template)]
109 #[template(path = "partials/export_download.html")]
110 pub struct ExportDownloadTemplate {
111 pub data_uri: String,
112 pub filename: String,
113 }
114
115 /// HTMX partial: download button shown when a content export is ready.
116 #[derive(Template)]
117 #[template(path = "partials/export_content_ready.html")]
118 pub struct ExportContentReadyTemplate {
119 pub download_url: String,
120 }
121
122 /// HTMX partial: inline login error message.
123 #[derive(Template)]
124 #[template(path = "partials/login_error.html")]
125 pub struct LoginErrorTemplate {
126 pub message: String,
127 }
128
129 impl LoginErrorTemplate {
130 pub fn render_string(&self) -> crate::error::Result<String> {
131 crate::helpers::render_fragment(self)
132 }
133 }
134
135 /// HTMX partial: username availability check result.
136 #[derive(Template)]
137 #[template(path = "partials/username_status.html")]
138 pub struct UsernameStatusTemplate {
139 pub available: bool,
140 }
141
142 impl UsernameStatusTemplate {
143 pub fn render_string(&self) -> crate::error::Result<String> {
144 crate::helpers::render_fragment(self)
145 }
146 }
147
148 /// HTMX partial: project slug availability check result.
149 #[derive(Template)]
150 #[template(path = "partials/slug_status.html")]
151 pub struct SlugStatusTemplate {
152 pub available: bool,
153 pub suggestions: Vec<String>,
154 }
155
156 impl SlugStatusTemplate {
157 pub fn render_string(&self) -> crate::error::Result<String> {
158 crate::helpers::render_fragment(self)
159 }
160 }
161
162 /// HTMX partial: inline save confirmation or error indicator.
163 #[derive(Template)]
164 #[template(path = "partials/save_status.html")]
165 pub struct SaveStatusTemplate {
166 pub success: bool,
167 pub message: String,
168 }
169
170 impl SaveStatusTemplate {
171 pub fn render_string(&self) -> crate::error::Result<String> {
172 crate::helpers::render_fragment(self)
173 }
174 }
175
176 /// HTMX partial: paginated transaction history table.
177 #[derive(Template)]
178 #[template(path = "partials/transactions_table.html")]
179 pub struct TransactionsTableTemplate {
180 pub transactions: Vec<Transaction>,
181 }
182
183 // Dashboard Tab Partials
184
185 /// Dashboard tab: public identity, links, bio, domain, feed.
186 #[derive(Template)]
187 #[template(path = "partials/tabs/user_profile.html")]
188 pub struct UserProfileTabTemplate {
189 pub user: User,
190 pub custom_links: Vec<CustomLinkWithId>,
191 /// HMAC-signed personal RSS feed URL for this user.
192 pub feed_url: String,
193 /// Whether this user has creator access (controls custom domain visibility).
194 pub can_create_projects: bool,
195 /// The user's custom domain, if configured.
196 pub custom_domain: Option<CustomDomainInfo>,
197 /// Built-in theme choices for the profile theme picker (Tier 0).
198 pub theme_options: Vec<crate::theming::ThemeOption>,
199 }
200
201 /// Dashboard settings meta-tab with sub-navigation (profile, account, plan, etc.)
202 #[derive(Template)]
203 #[template(path = "partials/tabs/user_settings.html")]
204 pub struct UserSettingsTabTemplate {
205 pub user: User,
206 pub custom_links: Vec<CustomLinkWithId>,
207 pub feed_url: String,
208 pub can_create_projects: bool,
209 pub custom_domain: Option<CustomDomainInfo>,
210 pub has_media: bool,
211 pub git_enabled: bool,
212 pub has_mt_memberships: bool,
213 /// Built-in theme choices for the profile theme picker (Tier 0). Shared with
214 /// the included `user_profile.html` partial.
215 pub theme_options: Vec<crate::theming::ThemeOption>,
216 }
217
218 /// One row of the "mail you will receive regardless" list.
219 ///
220 /// Rendered from `email::OperationalKind`, never hand-maintained: the enum
221 /// carries the copy so the page cannot fall out of step with what the send path
222 /// actually does. Hand-written prose here would be wrong within two releases.
223 pub struct OperationalMailRow {
224 pub label: &'static str,
225 pub justification: &'static str,
226 }
227
228 impl OperationalMailRow {
229 /// Every message a user cannot turn off, in enum order.
230 pub fn all() -> Vec<Self> {
231 crate::email::OperationalKind::ALL
232 .iter()
233 .filter(|k| k.user_facing())
234 .map(|k| Self {
235 label: k.label(),
236 justification: k.justification(),
237 })
238 .collect()
239 }
240 }
241
242 /// Dashboard tab: account mechanics, security, sessions, notifications, data.
243 #[derive(Template)]
244 #[template(path = "partials/tabs/user_account.html")]
245 pub struct UserAccountTabTemplate {
246 pub user: User,
247 /// The cannot-opt-out set, rendered under the preference form so the
248 /// toggles above are not read as an exhaustive list of the mail we send.
249 pub operational_mail: Vec<OperationalMailRow>,
250 /// Notification toggles, read from subscriptions rather than off `user`.
251 /// The columns they used to live on were dropped in migration 189; the
252 /// subscription is the record now.
253 pub notifications: crate::db::lists::NotificationPrefs,
254 pub sessions: Vec<DbUserSession>,
255 pub current_session_id: Option<UserSessionId>,
256 /// Whether this user has creator access (controls creator-specific prefs).
257 pub can_create_projects: bool,
258 /// Whether the user's email address has been verified.
259 pub email_verified: bool,
260 /// Active (unresolved) moderation actions for the "Account Status" section.
261 pub moderation_active: Vec<ModerationActionView>,
262 /// Resolved moderation history (collapsed by default).
263 pub moderation_history: Vec<ModerationActionView>,
264 /// Whether this creator has voluntarily paused their account.
265 pub creator_paused: bool,
266 /// Compact Fan+ pane state for the account tab. `None` = the user is not
267 /// a Fan+ subscriber; the tab renders a one-line "Support the platform"
268 /// link instead of the active pane.
269 pub fan_plus: Option<FanPlusPaneView>,
270 /// CSRF token for the Fan+ cancel/resume/billing-portal form posts. The
271 /// rest of the tab uses HTMX (which sends `X-CSRF-Token` automatically),
272 /// but these are vanilla form POSTs that redirect.
273 pub csrf_token: super::CsrfTokenOption,
274 }
275
276 /// Compact dashboard view of the user's Fan+ subscription. Lives under the
277 /// account tab; intentionally small, no upsell copy.
278 pub struct FanPlusPaneView {
279 /// Current period end as a formatted date (e.g., "Dec 14, 2026"). `None`
280 /// when Stripe hasn't reported a period yet (rare; just after checkout).
281 pub period_end: Option<String>,
282 /// Subscription is scheduled to cancel at `period_end`. Drives the Resume
283 /// affordance.
284 pub cancel_at_period_end: bool,
285 }
286
287 /// View model for a moderation action displayed on the settings page.
288 pub struct ModerationActionView {
289 /// Human-readable label (e.g., "Warning", "Content Removal", "Suspension")
290 pub action_label: String,
291 pub reason: String,
292 pub created_at: String,
293 pub resolved_at: Option<String>,
294 }
295
296 /// Custom domain info for dashboard display.
297 pub struct CustomDomainInfo {
298 pub id: String,
299 pub domain: String,
300 pub verified: bool,
301 pub verification_token: String,
302 pub instructions: String,
303 }
304
305 /// Custom link with ID for dashboard editing
306 #[derive(Clone)]
307 pub struct CustomLinkWithId {
308 pub id: String,
309 pub url: String,
310 pub title: String,
311 }
312
313 /// Dashboard tab: payment history, payouts, tips, and revenue splits.
314 #[derive(Template)]
315 #[template(path = "partials/tabs/user_payments.html")]
316 pub struct UserPaymentsTabTemplate {
317 /// Needed for the accept/decline forms on split invitations.
318 pub csrf_token: CsrfTokenOption,
319 pub user: User,
320 pub transactions: Vec<Transaction>,
321 pub tips_received: Vec<TipReceived>,
322 pub tips_total: String,
323 pub tips_count: i64,
324 /// Revenue owed to you from other creators' projects (as a collaborator).
325 /// May span currencies: a split is denominated in the paying project's
326 /// currency, not yours.
327 pub splits_incoming_total: String,
328 /// Whether any of that is in a currency you do not settle in, meaning
329 /// Stripe converts it at your payout and you carry the conversion.
330 pub splits_incoming_foreign: bool,
331 /// Split invitations awaiting this creator's answer. Shown at the top of
332 /// the tab, because it is the one thing here that needs a decision.
333 pub pending_invitations: Vec<crate::db::project_members::PendingSplitInvitation>,
334 /// This creator's own settlement currency, for comparing against each
335 /// invitation's project currency in the template.
336 pub own_currency: crate::currency::SettlementCurrency,
337 /// Whether anything is owed out at all. A bool rather than the template
338 /// comparing the formatted string against "$0.00", which stopped being a
339 /// reliable test for zero the moment a creator could settle in pounds.
340 pub splits_outgoing_any: bool,
341 pub splits_incoming_count: i64,
342 /// Revenue you owe to collaborators on your projects.
343 pub splits_outgoing_total: String,
344 /// Whether this user has creator access (controls seller section visibility).
345 pub can_create_projects: bool,
346 }
347
348 /// Dashboard tab: user's projects list with create button.
349 #[derive(Template)]
350 #[template(path = "partials/tabs/user_projects.html")]
351 pub struct UserProjectsTabTemplate {
352 pub projects: Vec<ProjectCard>,
353 pub can_create_projects: bool,
354 }
355
356 /// Creator tab in the user dashboard showing invite/waitlist status.
357 #[derive(Template)]
358 #[template(path = "partials/tabs/user_creator.html")]
359 #[allow(dead_code)] // Fields used by Askama template
360 pub struct UserCreatorTabTemplate {
361 pub csrf_token: CsrfTokenOption,
362 /// Whether this user has been granted creator privileges (can publish content).
363 pub can_create_projects: bool,
364 /// Whether the user's email is verified (required before joining waitlist).
365 pub email_verified: bool,
366 /// Number of unique followers who would receive a broadcast email.
367 pub follower_count: i64,
368 /// The user's waitlist entry, if they have applied for creator access.
369 pub waitlist_entry: Option<WaitlistEntry>,
370 /// Whether the invite system is enabled.
371 pub invites_enabled: bool,
372 /// Number of active (unredeemed) invite codes this creator has.
373 pub active_invite_count: i64,
374 /// Maximum number of unredeemed invite codes per creator.
375 pub invite_limit: i64,
376 /// Creator's invite codes for display.
377 pub invite_codes: Vec<InviteCodeDisplay>,
378 /// Total number of users with creator access.
379 pub total_creators: u32,
380 /// Number of waitlist entries still pending review.
381 pub waitlist_pending: u32,
382 /// Wave history for non-creator informational display.
383 pub waves: Vec<WaveStats>,
384 /// The creator's current tier label (e.g. "Basic", "Small Files"), if subscribed.
385 pub creator_tier_label: Option<String>,
386 /// The creator's subscription period end date (human-readable), if subscribed.
387 pub creator_period_end: Option<String>,
388 /// The creator's subscription status (e.g. Active, PastDue), if subscribed.
389 pub creator_sub_status: Option<crate::db::SubscriptionStatus>,
390 /// Whether creator tier Stripe checkout is configured.
391 pub creator_tiers_configured: bool,
392 /// Storage usage breakdown (audio, covers, downloads, insertions, video, media, gallery).
393 pub storage_audio: String,
394 pub storage_covers: String,
395 pub storage_downloads: String,
396 pub storage_insertions: String,
397 pub storage_video: String,
398 pub storage_media: String,
399 pub storage_gallery: String,
400 pub storage_total: String,
401 pub storage_max: String,
402 /// Storage usage percentage (0-100) for the progress bar.
403 pub storage_pct: u8,
404 /// Gauge severity for that bar, from `gauge_tier`: "warn", "danger" or "".
405 /// Read from the same function the SyncKit gauges use so the thresholds
406 /// live in one place.
407 pub storage_tier: &'static str,
408 /// Whether the founder pricing window is currently open. Used to badge
409 /// in-progress founders ("Founder pricing, locked in when the window
410 /// closes") before the close sweep stamps `founder_locked_at`.
411 pub founder_window_open: bool,
412 /// Whether this user holds an unsweep'd founder flag (subscribed during
413 /// the window, status not yet finalized by the close sweep).
414 pub is_founder: bool,
415 /// Whether this user's founder pricing is permanently locked in.
416 pub is_founder_locked: bool,
417 /// Tier cards rendered in the upgrade grid. Built from `state.tier_prices`
418 /// so a price change in `assumptions.toml` flows through automatically.
419 pub tier_cards: Vec<crate::tier_prices::TierCard>,
420 }
421
422 /// Dashboard tab: project overview with stat cards.
423 #[derive(Template)]
424 #[template(path = "partials/tabs/project_overview.html")]
425 pub struct ProjectOverviewTabTemplate {
426 pub stats: Vec<StatCard>,
427 pub project_slug: String,
428 pub stripe_connected: bool,
429 pub has_items: bool,
430 pub has_published_item: bool,
431 }
432
433 /// A soft-deleted item for the "Recently Deleted" section.
434 pub struct DeletedItemRow {
435 pub id: String,
436 pub title: String,
437 pub deleted_at: String,
438 }
439
440 /// Dashboard tab: project content items list.
441 #[derive(Template)]
442 #[template(path = "partials/tabs/project_content.html")]
443 pub struct ProjectContentTabTemplate {
444 pub items: Vec<ContentItem>,
445 pub deleted_items: Vec<DeletedItemRow>,
446 pub project_slug: String,
447 pub project_id: String,
448 pub posts: Vec<BlogPostDashboardRow>,
449 }
450
451 /// Dashboard tab: project analytics with stats, chart, and top items.
452 #[derive(Template)]
453 #[template(path = "partials/tabs/project_analytics.html")]
454 pub struct ProjectAnalyticsTabTemplate {
455 pub stats: Vec<StatCard>,
456 pub bars: Vec<ChartBar>,
457 pub items: Vec<ContentItem>,
458 pub project_slug: String,
459 pub active_range: String,
460 }
461
462 /// Dashboard tab: project settings, categories, labels, and features.
463 #[derive(Template)]
464 #[template(path = "partials/tabs/project_settings.html")]
465 pub struct ProjectSettingsTabTemplate {
466 pub project: Project,
467 /// Current category name for pre-populating the form, or empty.
468 pub category_name: String,
469 /// Project ID as string for HTMX targets.
470 pub project_id: String,
471 /// Active features on this project.
472 pub features: Vec<String>,
473 /// All available features as (value, label, description) tuples.
474 pub project_features: &'static [(&'static str, &'static str, &'static str)],
475 /// Tabbed markdown sections (privacy policy, terms, FAQ, etc).
476 pub sections: Vec<crate::db::DbProjectSection>,
477 /// Current pricing model as kebab string ("free", "buy_once", "pwyw", "subscription").
478 pub pricing_model: String,
479 /// Current buy-once price in dollars (formatted), empty if not set.
480 pub price_dollars: String,
481 /// Current PWYW minimum in dollars (formatted), empty if not set.
482 pub pwyw_min_dollars: String,
483 /// Built-in theme choices for the project theme picker (Tier 0).
484 pub theme_options: Vec<crate::theming::ThemeOption>,
485 }
486
487 /// Dashboard code tab partial (git repos management).
488 #[derive(Template)]
489 #[template(path = "partials/tabs/project_code.html")]
490 pub struct ProjectCodeTabTemplate {
491 pub project: Project,
492 pub git_enabled: bool,
493 pub linked_repos: Vec<LinkedRepoView>,
494 pub available_repos: Vec<crate::db::DbGitRepo>,
495 pub project_id: String,
496 }
497
498 /// Dashboard tab: SyncKit apps for a specific project.
499 #[derive(Template)]
500 #[template(path = "partials/tabs/project_synckit.html")]
501 pub struct ProjectSyncKitTabTemplate {
502 pub apps: Vec<SyncAppRow>,
503 pub project_id: String,
504 }
505
506 /// A linked repo with its collaborators, for the Code tab.
507 pub struct LinkedRepoView {
508 pub id: String,
509 pub name: String,
510 pub collaborators: Vec<RepoCollaboratorView>,
511 }
512
513 /// View model for a repo collaborator in the Code tab.
514 pub struct RepoCollaboratorView {
515 pub user_id: String,
516 pub username: String,
517 pub can_push: bool,
518 }
519
520 /// Dashboard blog tab partial.
521 #[derive(Template)]
522 #[template(path = "partials/tabs/project_blog.html")]
523 pub struct ProjectBlogTabTemplate {
524 pub project_id: String,
525 pub project_slug: String,
526 pub posts: Vec<BlogPostDashboardRow>,
527 }
528
529 /// Dashboard subscriptions tab partial for tier management.
530 #[derive(Template)]
531 #[template(path = "partials/tabs/project_subscriptions.html")]
532 #[allow(dead_code)] // Fields used by Askama template
533 pub struct ProjectSubscriptionsTabTemplate {
534 pub project_id: String,
535 pub project_slug: String,
536 pub tiers: Vec<SubscriptionTier>,
537 pub subscriber_count: i64,
538 pub stripe_connected: bool,
539 }
540
541 /// Dashboard members tab partial for managing project members and revenue splits.
542 #[derive(Template)]
543 #[template(path = "partials/tabs/project_members.html")]
544 #[allow(dead_code)]
545 pub struct ProjectMembersTabTemplate {
546 pub project_id: String,
547 pub project_slug: String,
548 pub members: Vec<ProjectMemberRow>,
549 pub owner_split: i64,
550 }
551
552 /// Combined monetization tab: tiers, promo codes, and team splits.
553 #[derive(Template)]
554 #[template(path = "partials/tabs/project_monetization.html")]
555 pub struct ProjectMonetizationTabTemplate {
556 pub project_id: String,
557 pub project_slug: String,
558 pub tiers: Vec<SubscriptionTier>,
559 pub subscriber_count: i64,
560 pub stripe_connected: bool,
561 pub promo_codes: Vec<crate::types::PromoCodeRow>,
562 pub items: Vec<ContentItem>,
563 pub members: Vec<ProjectMemberRow>,
564 pub owner_split: i64,
565 }
566
567 /// SyncKit tab in the user dashboard for managing sync apps.
568 #[derive(Template)]
569 #[template(path = "partials/tabs/user_synckit.html")]
570 pub struct UserSyncKitTabTemplate {
571 pub apps: Vec<SyncAppRow>,
572 pub projects: Vec<ProjectCard>,
573 }
574
575 /// Row in the Forums tab showing a community membership.
576 pub struct ForumMembership {
577 pub community_name: String,
578 pub profile_url: String,
579 pub role: String,
580 pub joined: String,
581 pub post_count: i64,
582 }
583
584 /// Forums tab in the user dashboard, lists MT community memberships.
585 #[derive(Template)]
586 #[template(path = "partials/tabs/user_forums.html")]
587 pub struct UserForumsTabTemplate {
588 pub memberships: Vec<ForumMembership>,
589 pub mt_base_url: String,
590 }
591
592 /// A media file row for the Media tab.
593 pub struct MediaFileRow {
594 pub id: String,
595 pub folder: String,
596 pub filename: String,
597 pub content_type: String,
598 pub file_size: String,
599 pub media_type: String,
600 pub cdn_url: String,
601 pub markdown_ref: String,
602 pub created_at: String,
603 }
604
605 /// Media tab in the user dashboard, media library with folders.
606 #[derive(Template)]
607 #[template(path = "partials/tabs/user_media.html")]
608 pub struct UserMediaTabTemplate {
609 pub files: Vec<MediaFileRow>,
610 pub folders: Vec<String>,
611 pub storage_display: String,
612 }
613
614 /// Support tab in the user dashboard, submit a support ticket.
615 #[derive(Template)]
616 #[template(path = "partials/tabs/user_support.html")]
617 pub struct UserSupportTabTemplate {
618 pub email: String,
619 }
620
621 // Library Tab Partials
622
623 /// Library purchases tab.
624 #[derive(Template)]
625 #[template(path = "partials/tabs/library_purchases.html")]
626 pub struct LibraryPurchasesTabTemplate {
627 pub purchases: Vec<crate::db::DbPurchaseRow>,
628 pub subscriptions: Vec<UserSubscription>,
629 }
630
631 /// Library collections tab.
632 #[derive(Template)]
633 #[template(path = "partials/tabs/library_collections.html")]
634 pub struct LibraryCollectionsTabTemplate {
635 pub collections: Vec<Collection>,
636 pub username: String,
637 pub wishlists: Vec<crate::db::wishlists::WishlistItem>,
638 }
639
640 /// Library contacts tab.
641 #[derive(Template)]
642 #[template(path = "partials/tabs/library_contacts.html")]
643 pub struct LibraryContactsTabTemplate {
644 pub shared_creators: Vec<crate::db::transactions::SharedCreatorRow>,
645 pub buyer_contacts: Vec<ContactRow>,
646 pub total_buyer_contacts: usize,
647 }
648
649 /// Library feed tab (items from followed users, projects, and tags).
650 #[derive(Template)]
651 #[template(path = "partials/tabs/library_feed.html")]
652 pub struct LibraryFeedTabTemplate {
653 pub items: Vec<DiscoverItem>,
654 pub total_items: u32,
655 pub current_page: u32,
656 pub total_pages: u32,
657 pub pagination_range: Vec<u32>,
658 pub showing_start: u32,
659 pub showing_end: u32,
660 }
661
662 /// Library communities tab (Multithreaded forum memberships).
663 #[derive(Template)]
664 #[template(path = "partials/tabs/library_communities.html")]
665 pub struct LibraryCommunitiesTabTemplate {
666 pub memberships: Vec<ForumMembership>,
667 pub mt_base_url: String,
668 }
669
670 /// Per-project revenue for the user analytics top projects list.
671 pub struct ProjectRevenue {
672 pub title: String,
673 pub revenue: String,
674 }
675
676 /// User-level analytics tab (aggregated across all projects).
677 #[derive(Template)]
678 #[template(path = "partials/tabs/user_analytics.html")]
679 pub struct UserAnalyticsTabTemplate {
680 pub stats: Vec<StatCard>,
681 pub bars: Vec<ChartBar>,
682 pub top_projects: Vec<ProjectRevenue>,
683 pub active_range: String,
684 pub project_comparisons: Vec<ProjectComparison>,
685 }
686
687 /// Buyer contacts section, HTMX-loaded into the Payments tab.
688 #[derive(Template)]
689 #[template(path = "partials/tabs/buyer_contacts.html")]
690 pub struct BuyerContactsPartialTemplate {
691 pub contacts: Vec<BuyerContact>,
692 }
693
694 /// Stripe payout-summary card, HTMX-loaded into the Payments tab so the tab
695 /// render never blocks on the Stripe balance round-trip (ultra-fuzz Run 11 Perf SER-2).
696 #[derive(Template)]
697 #[template(path = "partials/tabs/payout_summary.html")]
698 pub struct PayoutSummaryPartialTemplate {
699 pub payout_summary: Option<PayoutSummary>,
700 pub stripe_payouts_enabled: bool,
701 }
702
703 /// SSH keys list partial for HTMX updates.
704 #[derive(Template)]
705 #[template(path = "partials/ssh_keys_list.html")]
706 pub struct SshKeysListTemplate {
707 pub ssh_keys: Vec<crate::routes::api::ssh_keys::SshKeyView>,
708 }
709
710 /// Dashboard tab: SSH key management for git access.
711 #[derive(Template)]
712 #[template(path = "partials/tabs/user_ssh_keys_tab.html")]
713 pub struct UserSshKeysTabTemplate {
714 pub username: String,
715 /// Choices for the console theme picker: "follow the terminal" first, then
716 /// every bundled theme. Lives on this tab rather than the profile one
717 /// because it themes the SSH console the tab's keys grant access to, not
718 /// the public profile.
719 pub theme_options: Vec<crate::theming::ThemeOption>,
720 }
721
722 /// Git access-token list partial for HTMX updates. `new_token` carries a
723 /// freshly-minted plaintext to show once (set only in the create response).
724 #[derive(Template)]
725 #[template(path = "partials/git_tokens_list.html")]
726 pub struct GitTokensListTemplate {
727 pub tokens: Vec<crate::routes::api::git_tokens::GitTokenView>,
728 pub new_token: Option<String>,
729 }
730
731 /// Sessions list partial, used by session revocation API responses (HTMX swap into `#sessions-list`).
732 #[derive(Template)]
733 #[template(path = "partials/tabs/user_sessions.html")]
734 pub struct UserSessionsPartialTemplate {
735 pub sessions: Vec<DbUserSession>,
736 pub current_session_id: Option<UserSessionId>,
737 }
738
739 /// HTMX partial: single removable tag pill on an item.
740 #[derive(Template)]
741 #[template(path = "partials/tag.html")]
742 pub struct TagTemplate {
743 pub item_id: String,
744 pub tag_id: String,
745 pub tag_name: String,
746 pub is_primary: bool,
747 }
748
749 impl TagTemplate {
750 pub fn render_string(&self) -> crate::error::Result<String> {
751 crate::helpers::render_fragment(self)
752 }
753 }
754
755 /// HTMX partial: editable content item row in the project dashboard.
756 #[derive(Template)]
757 #[template(path = "partials/item_edit_row.html")]
758 pub struct ItemEditRowTemplate {
759 pub item: ContentItem,
760 }
761
762 /// HTMX partial: editable custom link row in the user details tab.
763 #[derive(Template)]
764 #[template(path = "partials/link_row.html")]
765 pub struct LinkRowTemplate {
766 pub id: String,
767 pub title: String,
768 pub url: String,
769 }
770
771 impl LinkRowTemplate {
772 pub fn render_string(&self) -> crate::error::Result<String> {
773 crate::helpers::render_fragment(self)
774 }
775 }
776
777 // Project Labels Partial
778
779 // Admin Partials
780
781 /// Admin HTMX partial: creator waitlist entries table.
782 #[derive(Template)]
783 #[template(path = "partials/admin_waitlist_entries.html")]
784 pub struct AdminWaitlistEntriesTemplate {
785 pub entries: Vec<AdminWaitlistRow>,
786 }
787
788 /// Admin user table rows (HTMX partial).
789 #[derive(Template)]
790 #[template(path = "partials/admin_user_entries.html")]
791 pub struct AdminUserEntriesTemplate {
792 pub users: Vec<AdminUserRow>,
793 pub current_page: i64,
794 pub total_pages: i64,
795 pub current_filter: String,
796 }
797
798 /// Admin upload review entries (HTMX partial).
799 #[derive(Template)]
800 #[template(path = "partials/admin_upload_entries.html")]
801 pub struct AdminUploadEntriesTemplate {
802 pub held_uploads: Vec<AdminHeldUploadRow>,
803 }
804
805 /// Active-queue counts partial, refreshed by the admin dashboard every 10s.
806 #[derive(Template)]
807 #[template(path = "partials/admin_queue_summary.html")]
808 pub struct AdminQueueSummaryTemplate {
809 pub queue_pending: i64,
810 pub queue_running: i64,
811 }
812
813 /// Admin appeal rows (HTMX partial).
814 #[derive(Template)]
815 #[template(path = "partials/admin_appeal_entries.html")]
816 pub struct AdminAppealEntriesTemplate {
817 pub appeals: Vec<AdminAppealRow>,
818 }
819
820 /// Admin report entries (HTMX partial).
821 #[derive(Template)]
822 #[template(path = "partials/admin_report_entries.html")]
823 pub struct AdminReportEntriesTemplate {
824 pub reports: Vec<AdminReportRow>,
825 }
826
827 /// Suspension banner shown on dashboard for suspended users.
828 #[derive(Template)]
829 #[template(path = "partials/suspension_banner.html")]
830 pub struct SuspensionBannerTemplate {
831 pub reason: String,
832 pub has_pending_appeal: bool,
833 pub appeal_decision: Option<String>,
834 pub appeal_response: Option<String>,
835 }
836
837 // Promo Code Partials
838
839 /// Promo codes table partial (shared by project promotions tab, item dashboard, and API responses).
840 #[derive(Template)]
841 #[template(path = "partials/promo_codes_list.html")]
842 pub struct PromoCodesListTemplate {
843 pub promo_codes: Vec<crate::types::PromoCodeRow>,
844 }
845
846 /// Promotions tab content for the project dashboard.
847 #[derive(Template)]
848 #[template(path = "partials/tabs/project_promotions.html")]
849 pub struct ProjectPromotionsTabTemplate {
850 pub project_id: String,
851 pub project_slug: String,
852 pub promo_codes: Vec<crate::types::PromoCodeRow>,
853 pub items: Vec<ContentItem>,
854 }
855
856 // License Key Partials
857
858 /// License keys table for the item dashboard.
859 #[derive(Template)]
860 #[template(path = "partials/item_license_keys.html")]
861 pub struct ItemLicenseKeysTemplate {
862 pub license_keys: Vec<crate::types::LicenseKeyRow>,
863 }
864
865 // Follow Button Partial
866
867 /// HTMX follow/unfollow toggle button.
868 #[derive(Template)]
869 #[template(path = "partials/follow_button.html")]
870 pub struct FollowButtonTemplate {
871 pub target_type: String,
872 pub target_id: String,
873 pub is_following: bool,
874 pub follower_count: i64,
875 }
876
877 // Tag Follow Toggle (compact, for discover sidebar)
878
879 /// Compact follow/unfollow toggle for tags in the discover sidebar.
880 #[derive(Template)]
881 #[template(path = "partials/tag_follow_toggle.html")]
882 pub struct TagFollowToggleTemplate {
883 pub tag_id: String,
884 pub is_following: bool,
885 }
886
887 // TOTP 2FA Partials
888
889 /// TOTP setup partial: QR code, manual key, backup codes, and confirmation form.
890 #[derive(Template)]
891 #[template(path = "partials/totp_setup.html")]
892 pub struct TotpSetupTemplate {
893 pub qr_base64: String,
894 pub secret_base32: String,
895 pub backup_codes: Vec<String>,
896 }
897
898 /// TOTP status partial: enabled/disabled state with action buttons.
899 #[derive(Template)]
900 #[template(path = "partials/totp_status.html")]
901 pub struct TotpStatusTemplate {
902 pub enabled: bool,
903 }
904
905 // Passkey Partials
906
907 /// A registered passkey for dashboard display.
908 pub struct PasskeyDisplay {
909 pub id: String,
910 pub name: String,
911 pub created_at: String,
912 pub last_used_at: Option<String>,
913 }
914
915 /// Passkey list partial for the dashboard settings tab.
916 #[derive(Template)]
917 #[template(path = "partials/passkey_list.html")]
918 pub struct PasskeyListTemplate {
919 pub passkeys: Vec<PasskeyDisplay>,
920 }
921
922 // Tag Suggestions
923
924 /// A suggested tag for an item, based on metadata matching.
925 #[derive(Clone)]
926 pub struct TagSuggestion {
927 pub id: String,
928 pub name: String,
929 }
930
931 /// Auto-suggested tags for the item dashboard.
932 #[derive(Template)]
933 #[template(path = "partials/tag_suggestions.html")]
934 pub struct TagSuggestionsTemplate {
935 pub suggestions: Vec<TagSuggestion>,
936 }
937
938 // Content Insertion Partials
939
940 /// Item-level analytics partial (stats + revenue chart).
941 #[derive(Template)]
942 #[template(path = "partials/item_analytics.html")]
943 pub struct ItemAnalyticsPartialTemplate {
944 pub stats: Vec<StatCard>,
945 pub bars: Vec<ChartBar>,
946 pub item_id: String,
947 pub active_range: String,
948 }
949
950 /// An insertion clip for display in the dashboard.
951 #[derive(Clone)]
952 pub struct InsertionDisplay {
953 pub id: String,
954 pub title: String,
955 pub media_type: String,
956 pub duration_display: String,
957 pub created_at: String,
958 }
959
960 /// Creator's insertion clip library list.
961 #[derive(Template)]
962 #[template(path = "partials/insertion_list.html")]
963 pub struct InsertionListTemplate {
964 pub insertions: Vec<InsertionDisplay>,
965 }
966
967 /// A placement for display in the item dashboard.
968 #[derive(Clone)]
969 pub struct PlacementDisplay {
970 pub id: String,
971 pub insertion_title: String,
972 pub position: String,
973 pub offset_display: Option<String>,
974 pub sort_order: i32,
975 }
976
977 /// Per-item placement management list.
978 #[derive(Template)]
979 #[template(path = "partials/placement_list.html")]
980 pub struct PlacementListTemplate {
981 pub item_id: String,
982 pub placements: Vec<PlacementDisplay>,
983 pub available_insertions: Vec<InsertionDisplay>,
984 }
985
986 // Item Dashboard Tab Partials
987
988 /// Item overview tab: quick actions + analytics (lazy-loaded).
989 #[derive(Template)]
990 #[template(path = "partials/tabs/item_overview.html")]
991 pub struct ItemOverviewTabTemplate {
992 pub item: Item,
993 }
994
995 /// Item details tab: name, description, tags, content editor, bundle contents, sections.
996 #[derive(Template)]
997 #[template(path = "partials/tabs/item_details.html")]
998 pub struct ItemDetailsTabTemplate {
999 pub item: Item,
1000 pub bundle_items: Vec<Item>,
1001 pub bundleable_items: Vec<Item>,
1002 pub sections: Vec<ItemSection>,
1003 }
1004
1005 /// Item pricing tab: PWYW settings, license keys, promo codes.
1006 #[derive(Template)]
1007 #[template(path = "partials/tabs/item_pricing.html")]
1008 pub struct ItemPricingTabTemplate {
1009 pub item: Item,
1010 pub license_keys: Vec<LicenseKeyRow>,
1011 pub promo_codes: Vec<PromoCodeRow>,
1012 pub license_preset_options: Vec<(&'static str, &'static str)>,
1013 }
1014
1015 /// Item files tab: version upload + download table.
1016 #[derive(Template)]
1017 #[template(path = "partials/tabs/item_files.html")]
1018 pub struct ItemFilesTabTemplate {
1019 pub item: Item,
1020 pub versions: Vec<Version>,
1021 }
1022
1023 /// Item sales tab: transaction history with refund actions.
1024 #[derive(Template)]
1025 #[template(path = "partials/tabs/item_sales.html")]
1026 pub struct ItemSalesTabTemplate {
1027 pub item: Item,
1028 pub sales: Vec<SaleRow>,
1029 }
1030
1031 /// Item embed tab: copy-paste embed codes for this item.
1032 #[derive(Template)]
1033 #[template(path = "partials/tabs/item_embed.html")]
1034 pub struct ItemEmbedTabTemplate {
1035 pub item: Item,
1036 pub host_url: Arc<str>,
1037 pub is_audio: bool,
1038 }
1039