Skip to main content

max / makenotwork

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