Skip to main content

max / makenotwork

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