Skip to main content

max / makenotwork

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