Skip to main content

max / makenotwork

31.0 KB · 925 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 /// Whether the invite system is enabled.
358 pub invites_enabled: bool,
359 /// Number of active (unredeemed) invite codes this creator has.
360 pub active_invite_count: i64,
361 /// Maximum number of unredeemed invite codes per creator.
362 pub invite_limit: i64,
363 /// Creator's invite codes for display.
364 pub invite_codes: Vec<InviteCodeDisplay>,
365 /// Total number of users with creator access.
366 pub total_creators: u32,
367 /// Number of waitlist entries still pending review.
368 pub waitlist_pending: u32,
369 /// Wave history for non-creator informational display.
370 pub waves: Vec<WaveStats>,
371 /// The creator's current tier label (e.g. "Basic", "Small Files"), if subscribed.
372 pub creator_tier_label: Option<String>,
373 /// The creator's subscription period end date (human-readable), if subscribed.
374 pub creator_period_end: Option<String>,
375 /// The creator's subscription status (e.g. Active, PastDue), if subscribed.
376 pub creator_sub_status: Option<crate::db::SubscriptionStatus>,
377 /// Whether creator tier Stripe checkout is configured.
378 pub creator_tiers_configured: bool,
379 /// Storage usage breakdown (audio, covers, downloads, insertions, video, media, gallery).
380 pub storage_audio: String,
381 pub storage_covers: String,
382 pub storage_downloads: String,
383 pub storage_insertions: String,
384 pub storage_video: String,
385 pub storage_media: String,
386 pub storage_gallery: String,
387 pub storage_total: String,
388 pub storage_max: String,
389 /// Storage usage percentage (0-100) for the progress bar.
390 pub storage_pct: u8,
391 /// Gauge severity for that bar, from `gauge_tier`: "warn", "danger" or "".
392 /// Read from the same function the SyncKit gauges use so the thresholds
393 /// live in one place.
394 pub storage_tier: &'static str,
395 /// Whether the founder pricing window is currently open. Used to badge
396 /// in-progress founders ("Founder pricing, locked in when the window
397 /// closes") before the close sweep stamps `founder_locked_at`.
398 pub founder_window_open: bool,
399 /// Whether this user holds an unsweep'd founder flag (subscribed during
400 /// the window, status not yet finalized by the close sweep).
401 pub is_founder: bool,
402 /// Whether this user's founder pricing is permanently locked in.
403 pub is_founder_locked: bool,
404 /// Tier cards rendered in the upgrade grid. Built from `state.tier_prices`
405 /// so a price change in `assumptions.toml` flows through automatically.
406 pub tier_cards: Vec<crate::tier_prices::TierCard>,
407 }
408
409 /// Dashboard tab: project overview with stat cards.
410 ///
411 /// Not a `Template` any more, following `ProjectContentTabTemplate` above: the
412 /// described panel (`crate::quasi::project_overview`) is the only rendering, and
413 /// both callers of `build_overview` take these five fields off this struct and
414 /// hand them to it. Kept as the shape of that gather rather than folded into the
415 /// callers, because two call sites want the same three queries.
416 pub struct ProjectOverviewTabTemplate {
417 pub stats: Vec<StatCard>,
418 pub project_slug: String,
419 pub stripe_connected: bool,
420 pub has_items: bool,
421 pub has_published_item: bool,
422 }
423
424 /// A soft-deleted item for the "Recently Deleted" section.
425 pub struct DeletedItemRow {
426 pub id: String,
427 pub title: String,
428 pub deleted_at: String,
429 }
430
431 /// The project Content tab's data, gathered once and rendered by the description.
432 ///
433 /// Not a `Template` any more. It derived one and rendered
434 /// `partials/tabs/project_content.html` until `64b33b26`; the described panel
435 /// (`crate::quasi::project_content`) is the only rendering now, and both callers
436 /// of `build_content` take `items`, `deleted_items` and `posts` off this struct
437 /// and hand them to it. Kept as the shape of that gather rather than folded into
438 /// the caller, because two call sites want the same four queries.
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 settings, categories, labels, and features.
448 #[derive(Template)]
449 #[template(path = "partials/tabs/project_settings.html")]
450 pub struct ProjectSettingsTabTemplate {
451 pub project: Project,
452 /// Current category name for pre-populating the form, or empty.
453 pub category_name: String,
454 /// Project ID as string for HTMX targets.
455 pub project_id: String,
456 /// Active features on this project.
457 pub features: Vec<String>,
458 /// All available features as (value, label, description) tuples.
459 pub project_features: &'static [(&'static str, &'static str, &'static str)],
460 /// Tabbed markdown sections (privacy policy, terms, FAQ, etc).
461 pub sections: Vec<crate::db::DbProjectSection>,
462 /// Current pricing model as kebab string ("free", "buy_once", "pwyw", "subscription").
463 pub pricing_model: String,
464 /// Current buy-once price in dollars (formatted), empty if not set.
465 pub price_dollars: String,
466 /// Current PWYW minimum in dollars (formatted), empty if not set.
467 pub pwyw_min_dollars: String,
468 /// Built-in theme choices for the project theme picker (Tier 0).
469 pub theme_options: Vec<crate::theming::ThemeOption>,
470 }
471
472 /// Dashboard code tab partial (git repos management).
473 #[derive(Template)]
474 #[template(path = "partials/tabs/project_code.html")]
475 pub struct ProjectCodeTabTemplate {
476 pub project: Project,
477 pub git_enabled: bool,
478 pub linked_repos: Vec<LinkedRepoView>,
479 pub available_repos: Vec<crate::db::DbGitRepo>,
480 pub project_id: String,
481 }
482
483 /// Dashboard tab: SyncKit apps for a specific project.
484 #[derive(Template)]
485 #[template(path = "partials/tabs/project_synckit.html")]
486 pub struct ProjectSyncKitTabTemplate {
487 pub apps: Vec<SyncAppRow>,
488 pub project_id: String,
489 /// Where a self-refresh re-fetches this panel from. The tab's JS used to
490 /// click `#tab-synckit`; the strip is described now and the button is gone.
491 pub project_slug: String,
492 }
493
494 /// A linked repo with its collaborators, for the Code tab.
495 pub struct LinkedRepoView {
496 pub id: String,
497 pub name: String,
498 pub collaborators: Vec<RepoCollaboratorView>,
499 }
500
501 /// View model for a repo collaborator in the Code tab.
502 pub struct RepoCollaboratorView {
503 pub user_id: String,
504 pub username: String,
505 pub can_push: bool,
506 }
507
508 /// Dashboard subscriptions tab partial for tier management.
509 #[derive(Template)]
510 #[template(path = "partials/tabs/project_subscriptions.html")]
511 #[allow(dead_code)] // Fields used by Askama template
512 pub struct ProjectSubscriptionsTabTemplate {
513 pub project_id: String,
514 pub project_slug: String,
515 pub tiers: Vec<SubscriptionTier>,
516 pub subscriber_count: i64,
517 pub stripe_connected: bool,
518 }
519
520 /// Combined monetization tab: tiers, promo codes, and team splits.
521 #[derive(Template)]
522 #[template(path = "partials/tabs/project_monetization.html")]
523 pub struct ProjectMonetizationTabTemplate {
524 pub project_id: String,
525 pub project_slug: String,
526 pub tiers: Vec<SubscriptionTier>,
527 pub subscriber_count: i64,
528 pub stripe_connected: bool,
529 pub promo_codes: Vec<crate::types::PromoCodeRow>,
530 pub items: Vec<ContentItem>,
531 pub members: Vec<ProjectMemberRow>,
532 pub owner_split: i64,
533 }
534
535 /// SyncKit tab in the user dashboard for managing sync apps.
536 #[derive(Template)]
537 #[template(path = "partials/tabs/user_synckit.html")]
538 pub struct UserSyncKitTabTemplate {
539 pub apps: Vec<SyncAppRow>,
540 pub projects: Vec<ProjectCard>,
541 }
542
543 /// Row in the Forums tab showing a community membership.
544 pub struct ForumMembership {
545 pub community_name: String,
546 pub profile_url: String,
547 pub role: String,
548 pub joined: String,
549 pub post_count: i64,
550 }
551
552 /// A media file row for the Media tab.
553 pub struct MediaFileRow {
554 pub id: String,
555 pub folder: String,
556 pub filename: String,
557 pub content_type: String,
558 pub file_size: String,
559 pub media_type: String,
560 pub cdn_url: String,
561 pub markdown_ref: String,
562 pub created_at: String,
563 }
564
565 /// Media tab in the user dashboard, media library with folders.
566 #[derive(Template)]
567 #[template(path = "partials/tabs/user_media.html")]
568 pub struct UserMediaTabTemplate {
569 pub files: Vec<MediaFileRow>,
570 pub folders: Vec<String>,
571 pub storage_display: String,
572 }
573
574 // Library Tab Partials
575
576 /// Library purchases tab.
577 #[derive(Template)]
578 #[template(path = "partials/tabs/library_purchases.html")]
579 pub struct LibraryPurchasesTabTemplate {
580 pub purchases: Vec<crate::db::DbPurchaseRow>,
581 pub subscriptions: Vec<UserSubscription>,
582 }
583
584 /// Library collections tab.
585 #[derive(Template)]
586 #[template(path = "partials/tabs/library_collections.html")]
587 pub struct LibraryCollectionsTabTemplate {
588 pub collections: Vec<Collection>,
589 pub username: String,
590 pub wishlists: Vec<crate::db::wishlists::WishlistItem>,
591 }
592
593 /// Library feed tab (items from followed users, projects, and tags).
594 #[derive(Template)]
595 #[template(path = "partials/tabs/library_feed.html")]
596 pub struct LibraryFeedTabTemplate {
597 pub items: Vec<DiscoverItem>,
598 pub total_items: u32,
599 pub current_page: u32,
600 pub total_pages: u32,
601 pub pagination_range: Vec<u32>,
602 pub showing_start: u32,
603 pub showing_end: u32,
604 }
605
606 /// Per-project revenue for the user analytics top projects list.
607 pub struct ProjectRevenue {
608 pub title: String,
609 pub revenue: String,
610 }
611
612 /// SSH keys list partial for HTMX updates.
613 #[derive(Template)]
614 #[template(path = "partials/ssh_keys_list.html")]
615 pub struct SshKeysListTemplate {
616 pub ssh_keys: Vec<crate::routes::api::ssh_keys::SshKeyView>,
617 }
618
619 /// Git access-token list partial for HTMX updates. `new_token` carries a
620 /// freshly-minted plaintext to show once (set only in the create response).
621 #[derive(Template)]
622 #[template(path = "partials/git_tokens_list.html")]
623 pub struct GitTokensListTemplate {
624 pub tokens: Vec<crate::routes::api::git_tokens::GitTokenView>,
625 pub new_token: Option<String>,
626 }
627
628 /// Sessions list partial, used by session revocation API responses (HTMX swap into `#sessions-list`).
629 #[derive(Template)]
630 #[template(path = "partials/tabs/user_sessions.html")]
631 pub struct UserSessionsPartialTemplate {
632 pub sessions: Vec<DbUserSession>,
633 pub current_session_id: Option<UserSessionId>,
634 }
635
636 /// HTMX partial: single removable tag pill on an item.
637 #[derive(Template)]
638 #[template(path = "partials/tag.html")]
639 pub struct TagTemplate {
640 pub item_id: String,
641 pub tag_id: String,
642 pub tag_name: String,
643 pub is_primary: bool,
644 }
645
646 impl TagTemplate {
647 pub fn render_string(&self) -> crate::error::Result<String> {
648 crate::helpers::render_fragment(self)
649 }
650 }
651
652 /// HTMX partial: editable content item row in the project dashboard.
653 #[derive(Template)]
654 #[template(path = "partials/item_edit_row.html")]
655 pub struct ItemEditRowTemplate {
656 pub item: ContentItem,
657 }
658
659 /// HTMX partial: editable custom link row in the user details tab.
660 #[derive(Template)]
661 #[template(path = "partials/link_row.html")]
662 pub struct LinkRowTemplate {
663 pub id: String,
664 pub title: String,
665 pub url: String,
666 }
667
668 impl LinkRowTemplate {
669 pub fn render_string(&self) -> crate::error::Result<String> {
670 crate::helpers::render_fragment(self)
671 }
672 }
673
674 // Project Labels Partial
675
676 // Admin Partials
677
678 /// Admin HTMX partial: creator waitlist entries table.
679 #[derive(Template)]
680 #[template(path = "partials/admin_waitlist_entries.html")]
681 pub struct AdminWaitlistEntriesTemplate {
682 pub entries: Vec<AdminWaitlistRow>,
683 }
684
685 /// Admin user table rows (HTMX partial).
686 #[derive(Template)]
687 #[template(path = "partials/admin_user_entries.html")]
688 pub struct AdminUserEntriesTemplate {
689 pub users: Vec<AdminUserRow>,
690 pub current_page: i64,
691 pub total_pages: i64,
692 pub current_filter: String,
693 }
694
695 /// Admin upload review entries (HTMX partial).
696 #[derive(Template)]
697 #[template(path = "partials/admin_upload_entries.html")]
698 pub struct AdminUploadEntriesTemplate {
699 pub held_uploads: Vec<AdminHeldUploadRow>,
700 }
701
702 /// Active-queue counts partial, refreshed by the admin dashboard every 10s.
703 #[derive(Template)]
704 #[template(path = "partials/admin_queue_summary.html")]
705 pub struct AdminQueueSummaryTemplate {
706 pub queue_pending: i64,
707 pub queue_running: i64,
708 }
709
710 /// Admin appeal rows (HTMX partial).
711 #[derive(Template)]
712 #[template(path = "partials/admin_appeal_entries.html")]
713 pub struct AdminAppealEntriesTemplate {
714 pub appeals: Vec<AdminAppealRow>,
715 }
716
717 /// Admin report entries (HTMX partial).
718 #[derive(Template)]
719 #[template(path = "partials/admin_report_entries.html")]
720 pub struct AdminReportEntriesTemplate {
721 pub reports: Vec<AdminReportRow>,
722 }
723
724 /// Suspension banner shown on dashboard for suspended users.
725 #[derive(Template)]
726 #[template(path = "partials/suspension_banner.html")]
727 pub struct SuspensionBannerTemplate {
728 pub reason: String,
729 pub has_pending_appeal: bool,
730 pub appeal_decision: Option<String>,
731 pub appeal_response: Option<String>,
732 }
733
734 // Promo Code Partials
735
736 /// Promo codes table partial (shared by project promotions tab, item dashboard, and API responses).
737 #[derive(Template)]
738 #[template(path = "partials/promo_codes_list.html")]
739 pub struct PromoCodesListTemplate {
740 pub promo_codes: Vec<crate::types::PromoCodeRow>,
741 }
742
743 // `ProjectPromotionsTabTemplate` was here until 2026-08-26. It rendered
744 // `partials/tabs/project_promotions.html` for a route nothing reached. The
745 // template itself is still live and is NOT deleted: `project_monetization.html`
746 // includes it, and `ProjectMonetizationTabTemplate` carries the `promo_codes`
747 // and `items` it needs. So the composite was always the only caller, and this
748 // struct was the second way to render a partial that only ever had one.
749
750 // License Key Partials
751
752 /// License keys table for the item dashboard.
753 #[derive(Template)]
754 #[template(path = "partials/item_license_keys.html")]
755 pub struct ItemLicenseKeysTemplate {
756 pub license_keys: Vec<crate::types::LicenseKeyRow>,
757 }
758
759 // Follow Button Partial
760
761 /// HTMX follow/unfollow toggle button.
762 #[derive(Template)]
763 #[template(path = "partials/follow_button.html")]
764 pub struct FollowButtonTemplate {
765 pub target_type: String,
766 pub target_id: String,
767 pub is_following: bool,
768 pub follower_count: i64,
769 }
770
771 // Tag Follow Toggle (compact, for discover sidebar)
772
773 /// Compact follow/unfollow toggle for tags in the discover sidebar.
774 #[derive(Template)]
775 #[template(path = "partials/tag_follow_toggle.html")]
776 pub struct TagFollowToggleTemplate {
777 pub tag_id: String,
778 pub is_following: bool,
779 }
780
781 // TOTP 2FA Partials
782
783 /// TOTP setup partial: QR code, manual key, backup codes, and confirmation form.
784 #[derive(Template)]
785 #[template(path = "partials/totp_setup.html")]
786 pub struct TotpSetupTemplate {
787 pub qr_base64: String,
788 pub secret_base32: String,
789 pub backup_codes: Vec<String>,
790 }
791
792 /// TOTP status partial: enabled/disabled state with action buttons.
793 #[derive(Template)]
794 #[template(path = "partials/totp_status.html")]
795 pub struct TotpStatusTemplate {
796 pub enabled: bool,
797 }
798
799 // Passkey Partials
800
801 /// A registered passkey for dashboard display.
802 pub struct PasskeyDisplay {
803 pub id: String,
804 pub name: String,
805 pub created_at: String,
806 pub last_used_at: Option<String>,
807 }
808
809 /// Passkey list partial for the dashboard settings tab.
810 #[derive(Template)]
811 #[template(path = "partials/passkey_list.html")]
812 pub struct PasskeyListTemplate {
813 pub passkeys: Vec<PasskeyDisplay>,
814 }
815
816 // Tag Suggestions
817
818 /// A suggested tag for an item, based on metadata matching.
819 #[derive(Clone)]
820 pub struct TagSuggestion {
821 pub id: String,
822 pub name: String,
823 }
824
825 /// Auto-suggested tags for the item dashboard.
826 #[derive(Template)]
827 #[template(path = "partials/tag_suggestions.html")]
828 pub struct TagSuggestionsTemplate {
829 pub suggestions: Vec<TagSuggestion>,
830 }
831
832 // Content Insertion Partials
833
834 /// Item-level analytics partial (stats + revenue chart).
835 #[derive(Template)]
836 #[template(path = "partials/item_analytics.html")]
837 pub struct ItemAnalyticsPartialTemplate {
838 pub stats: Vec<StatCard>,
839 pub bars: Vec<ChartBar>,
840 pub item_id: String,
841 pub active_range: String,
842 }
843
844 /// An insertion clip for display in the dashboard.
845 #[derive(Clone)]
846 pub struct InsertionDisplay {
847 pub id: String,
848 pub title: String,
849 pub media_type: String,
850 pub duration_display: String,
851 pub created_at: String,
852 }
853
854 /// Creator's insertion clip library list.
855 #[derive(Template)]
856 #[template(path = "partials/insertion_list.html")]
857 pub struct InsertionListTemplate {
858 pub insertions: Vec<InsertionDisplay>,
859 }
860
861 /// A placement for display in the item dashboard.
862 #[derive(Clone)]
863 pub struct PlacementDisplay {
864 pub id: String,
865 pub insertion_title: String,
866 pub position: String,
867 pub offset_display: Option<String>,
868 pub sort_order: i32,
869 }
870
871 /// Per-item placement management list.
872 #[derive(Template)]
873 #[template(path = "partials/placement_list.html")]
874 pub struct PlacementListTemplate {
875 pub item_id: String,
876 pub placements: Vec<PlacementDisplay>,
877 pub available_insertions: Vec<InsertionDisplay>,
878 }
879
880 // Item Dashboard Tab Partials
881
882 /// Item overview tab: quick actions + analytics (lazy-loaded).
883 #[derive(Template)]
884 #[template(path = "partials/tabs/item_overview.html")]
885 pub struct ItemOverviewTabTemplate {
886 pub item: Item,
887 }
888
889 /// Item details tab: name, description, tags, content editor, bundle contents, sections.
890 #[derive(Template)]
891 #[template(path = "partials/tabs/item_details.html")]
892 pub struct ItemDetailsTabTemplate {
893 pub item: Item,
894 pub bundle_items: Vec<Item>,
895 pub bundleable_items: Vec<Item>,
896 pub sections: Vec<ItemSection>,
897 }
898
899 /// Item pricing tab: PWYW settings, license keys, promo codes.
900 #[derive(Template)]
901 #[template(path = "partials/tabs/item_pricing.html")]
902 pub struct ItemPricingTabTemplate {
903 pub item: Item,
904 pub license_keys: Vec<LicenseKeyRow>,
905 pub promo_codes: Vec<PromoCodeRow>,
906 pub license_preset_options: Vec<(&'static str, &'static str)>,
907 }
908
909 /// Item files tab: version upload + download table.
910 #[derive(Template)]
911 #[template(path = "partials/tabs/item_files.html")]
912 pub struct ItemFilesTabTemplate {
913 pub item: Item,
914 pub versions: Vec<Version>,
915 }
916
917 /// Item embed tab: copy-paste embed codes for this item.
918 #[derive(Template)]
919 #[template(path = "partials/tabs/item_embed.html")]
920 pub struct ItemEmbedTabTemplate {
921 pub item: Item,
922 pub host_url: Arc<str>,
923 pub is_audio: bool,
924 }
925