Skip to main content

max / makenotwork

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