Skip to main content

max / makenotwork

16.5 KB · 517 lines History Blame Raw
1 //! Templates for creator dashboards, admin panels, and account management.
2
3 use askama::Template;
4
5 use crate::auth::SessionUser;
6 use crate::types::{
7 AdminAppealRow, AdminAuditLogRow, AdminCompCodeRow, AdminHeldUploadRow, AdminReportRow,
8 AdminSignupRow, AdminUserRow, AdminWaitlistRow, ContentItem, Item, OnboardingChecklist,
9 Project, ProjectCard, ReportStats, ScanHistoryDisplay, StatCard, Transaction, User,
10 WaitlistStats,
11 };
12
13 use super::CsrfTokenOption;
14
15 // Dashboard Pages
16
17 /// Main user dashboard page with tabbed navigation.
18 #[derive(Template)]
19 #[template(path = "dashboards/dashboard-user.html")]
20 #[allow(dead_code)] // Fields used by Askama template
21 pub struct DashboardUserTemplate {
22 pub csrf_token: CsrfTokenOption,
23 pub session_user: Option<SessionUser>,
24 pub user: User,
25 pub transactions: Vec<Transaction>,
26 /// Used to conditionally show the first-run onboarding banner.
27 pub projects: Vec<ProjectCard>,
28 /// Onboarding checklist for new creators (None once all steps complete).
29 pub onboarding: Option<OnboardingChecklist>,
30 /// Show "Show setup checklist" link when checklist was dismissed but steps remain.
31 pub show_checklist_recovery: bool,
32 /// Whether user has MT forum memberships (controls Forums tab visibility).
33 pub has_mt_memberships: bool,
34 // Suspension context (for banner)
35 pub suspended: bool,
36 pub suspension_reason: Option<String>,
37 pub has_pending_appeal: bool,
38 pub appeal_decision: Option<String>,
39 pub appeal_response: Option<String>,
40 /// One-time warning when a breached password is detected (cleared after display).
41 pub password_warning: Option<String>,
42 /// Whether the user has self-deactivated their account (limbo state).
43 pub deactivated: bool,
44 /// Whether this creator has voluntarily paused their account.
45 pub creator_paused: bool,
46 /// Whether git hosting is configured on this server (controls SSH Keys tab visibility).
47 pub git_enabled: bool,
48 }
49
50 /// Project dashboard page with stats, content list, and management tabs.
51 #[derive(Template)]
52 #[template(path = "dashboards/dashboard-project.html")]
53 #[allow(dead_code)] // Fields used by Askama template
54 pub struct DashboardProjectTemplate {
55 pub csrf_token: CsrfTokenOption,
56 pub session_user: Option<SessionUser>,
57 pub project: Project,
58 pub creator_username: String,
59 pub stats: Vec<StatCard>,
60 pub items: Vec<ContentItem>,
61 /// Whether this creator has connected their Stripe account (controls Subscriptions tab visibility).
62 pub stripe_connected: bool,
63 /// Whether this project has the "blog" feature enabled (controls Blog tab visibility).
64 pub has_blog: bool,
65 /// Whether git hosting is configured on this server (controls Code tab visibility).
66 pub git_enabled: bool,
67 /// Whether this project has the "cloud_sync" feature enabled (controls Cloud Sync tab visibility).
68 pub synckit_enabled: bool,
69 }
70
71 /// Item management dashboard shell with tabbed navigation (content loaded via HTMX).
72 #[derive(Template)]
73 #[template(path = "dashboards/dashboard-item.html")]
74 pub struct DashboardItemTemplate {
75 pub csrf_token: CsrfTokenOption,
76 pub session_user: Option<SessionUser>,
77 pub item: Item,
78 pub project_title: String,
79 pub project_slug: String,
80 }
81
82 // Admin
83
84 /// Admin waitlist management page with filtering and invite controls.
85 #[derive(Template)]
86 #[template(path = "dashboards/admin-waitlist.html")]
87 pub struct AdminWaitlistTemplate {
88 pub csrf_token: CsrfTokenOption,
89 pub session_user: Option<SessionUser>,
90 pub stats: WaitlistStats,
91 pub entries: Vec<AdminWaitlistRow>,
92 pub current_filter: String,
93 pub admin_active_page: &'static str,
94 }
95
96 /// Admin user management page.
97 #[derive(Template)]
98 #[template(path = "dashboards/admin-users.html")]
99 pub struct AdminUsersTemplate {
100 pub csrf_token: CsrfTokenOption,
101 pub session_user: Option<SessionUser>,
102 pub users: Vec<AdminUserRow>,
103 pub total_users: usize,
104 pub total_suspended: usize,
105 pub current_filter: String,
106 pub current_page: i64,
107 pub total_pages: i64,
108 pub admin_active_page: &'static str,
109 }
110
111 /// Per-layer health stat displayed in the dashboard's top panel.
112 #[derive(Clone)]
113 pub struct LayerHealthCard {
114 pub layer: String,
115 pub total: i64,
116 pub success_rate_pct: i32,
117 pub error_rate_pct: i32,
118 pub fail_count: i64,
119 pub status_badge: &'static str,
120 pub last_seen: String,
121 }
122
123 /// Admin upload review queue page.
124 #[derive(Template)]
125 #[template(path = "dashboards/admin-uploads.html")]
126 pub struct AdminUploadsTemplate {
127 pub csrf_token: CsrfTokenOption,
128 pub session_user: Option<SessionUser>,
129 pub held_uploads: Vec<AdminHeldUploadRow>,
130 pub total_held: usize,
131 pub admin_active_page: &'static str,
132 pub layer_health: Vec<LayerHealthCard>,
133 pub queue_pending: i64,
134 pub queue_running: i64,
135 pub recent_history: Vec<ScanHistoryDisplay>,
136 pub history_total: usize,
137 }
138
139 /// Full audit log for the scan pipeline (Phase 2b).
140 #[derive(Template)]
141 #[template(path = "dashboards/admin-scan-audit.html")]
142 pub struct AdminScanAuditTemplate {
143 pub csrf_token: CsrfTokenOption,
144 pub session_user: Option<SessionUser>,
145 pub admin_active_page: &'static str,
146 pub entries: Vec<AdminAuditLogRow>,
147 pub filter_action: String,
148 pub filter_admin: String,
149 pub filter_since_days: String,
150 }
151
152 /// Admin appeals queue page.
153 #[derive(Template)]
154 #[template(path = "dashboards/admin-appeals.html")]
155 pub struct AdminAppealsTemplate {
156 pub csrf_token: CsrfTokenOption,
157 pub session_user: Option<SessionUser>,
158 pub appeals: Vec<AdminAppealRow>,
159 pub admin_active_page: &'static str,
160 }
161
162 /// Admin reports queue page.
163 #[derive(Template)]
164 #[template(path = "dashboards/admin-reports.html")]
165 pub struct AdminReportsTemplate {
166 pub csrf_token: CsrfTokenOption,
167 pub session_user: Option<SessionUser>,
168 pub reports: Vec<AdminReportRow>,
169 pub stats: ReportStats,
170 pub current_filter: String,
171 pub admin_active_page: &'static str,
172 }
173
174 /// Admin email signups page.
175 #[derive(Template)]
176 #[template(path = "dashboards/admin-signups.html")]
177 pub struct AdminSignupsTemplate {
178 pub csrf_token: CsrfTokenOption,
179 pub session_user: Option<SessionUser>,
180 pub signups: Vec<AdminSignupRow>,
181 pub total: i64,
182 pub admin_active_page: &'static str,
183 }
184
185 /// Admin metrics dashboard page.
186 #[derive(Template)]
187 #[template(path = "dashboards/admin-metrics.html")]
188 pub struct AdminMetricsTemplate {
189 pub csrf_token: CsrfTokenOption,
190 pub session_user: Option<SessionUser>,
191 pub admin_active_page: &'static str,
192 pub uptime: String,
193 pub total_requests: u64,
194 pub error_rate: f64,
195 pub total_errors: u64,
196 pub pool_max: u32,
197 pub pool_active: u32,
198 pub pool_idle: u32,
199 pub top_routes: Vec<RouteMetric>,
200 pub error_breakdown: Vec<ErrorMetric>,
201 }
202
203 /// Admin comp-codes dashboard page (mint form + status list).
204 #[derive(Template)]
205 #[template(path = "dashboards/admin-comp-codes.html")]
206 pub struct AdminCompCodesTemplate {
207 pub csrf_token: CsrfTokenOption,
208 pub session_user: Option<SessionUser>,
209 pub admin_active_page: &'static str,
210 pub comp_codes: Vec<AdminCompCodeRow>,
211 }
212
213 /// The comp-codes table body, re-rendered after a mint to refresh the list.
214 #[derive(Template)]
215 #[template(path = "partials/admin_comp_codes_entries.html")]
216 pub struct AdminCompCodesEntriesTemplate {
217 pub comp_codes: Vec<AdminCompCodeRow>,
218 }
219
220 /// A row in the top routes table.
221 pub struct RouteMetric {
222 pub method: String,
223 pub path: String,
224 pub status: String,
225 pub count: u64,
226 }
227
228 /// A row in the error breakdown table.
229 pub struct ErrorMetric {
230 pub kind: String,
231 pub count: u64,
232 }
233
234 // Export & Account Management
235
236 /// Data export portal for the no-lock-in guarantee.
237 #[derive(Template)]
238 #[template(path = "dashboards/dashboard-export.html")]
239 pub struct ExportPortalTemplate {
240 pub csrf_token: CsrfTokenOption,
241 pub session_user: Option<SessionUser>,
242 /// Whether the user has any exportable content (projects, items, files).
243 pub has_content: bool,
244 /// Human-readable total size of exportable data (e.g. "12.3 MB").
245 pub content_size: String,
246 }
247
248 /// Data import portal for migrating from other platforms.
249 #[derive(Template)]
250 #[template(path = "dashboards/dashboard-import.html")]
251 pub struct ImportPortalTemplate {
252 pub csrf_token: CsrfTokenOption,
253 pub session_user: Option<SessionUser>,
254 pub projects: Vec<ImportProjectOption>,
255 pub jobs: Vec<ImportJobRow>,
256 }
257
258 /// Minimal project info for the import page project selector.
259 pub struct ImportProjectOption {
260 pub id: String,
261 pub title: String,
262 }
263
264 /// A row in the import history table.
265 pub struct ImportJobRow {
266 pub source: String,
267 pub status: String,
268 /// Badge class for `status`, from `ImportJobStatus::badge_status`.
269 pub status_class: &'static str,
270 pub total_rows: i32,
271 pub created_rows: i32,
272 pub created_at: chrono::DateTime<chrono::Utc>,
273 }
274
275 /// Account deletion confirmation page with username verification.
276 #[derive(Template)]
277 #[template(path = "dashboards/dashboard-delete-account.html")]
278 pub struct DeleteAccountTemplate {
279 pub csrf_token: CsrfTokenOption,
280 pub session_user: Option<SessionUser>,
281 pub username: String,
282 }
283
284 /// Blog post editor page (create/edit).
285 #[derive(Template)]
286 #[template(path = "dashboards/dashboard-blog-editor.html")]
287 pub struct BlogEditorTemplate {
288 pub csrf_token: CsrfTokenOption,
289 pub session_user: Option<SessionUser>,
290 pub project_id: String,
291 pub project_slug: String,
292 pub editing: bool,
293 pub post_id: String,
294 pub post_title: String,
295 pub post_slug: String,
296 pub post_body: String,
297 pub post_is_published: bool,
298 /// Whether this editor is for the platform changelog project. Gates the
299 /// owner-only "Show on landing" control so it never appears on a regular
300 /// creator's blog, where the flag would be inert.
301 pub is_changelog_project: bool,
302 /// Current value of the post's landing flag (false for new posts).
303 pub post_show_on_landing: bool,
304 }
305
306 /// HTMX partial: onboarding checklist (returned by the restore handler).
307 #[derive(Template)]
308 #[template(path = "partials/onboarding_checklist.html")]
309 pub struct OnboardingChecklistPartialTemplate {
310 pub checklist: OnboardingChecklist,
311 }
312
313 // License key and promo code view types live in crate::types (LicenseKeyRow, PromoCodeRow).
314
315 // Creation Wizards
316
317 /// Step navigation item for the wizard sidebar.
318 pub struct StepNavItem {
319 pub name: &'static str,
320 pub label: &'static str,
321 pub state: &'static str, // "completed", "active", "pending"
322 }
323
324 /// A subscription tier row for the wizard monetization/preview steps.
325 pub struct WizardTierRow {
326 pub id: String,
327 pub name: String,
328 pub price_display: String,
329 pub price_dollars: String,
330 pub description: String,
331 }
332
333 /// Full page: project creation wizard.
334 #[derive(Template)]
335 #[template(path = "wizards/wizard_project.html")]
336 pub struct WizardProjectTemplate {
337 pub csrf_token: CsrfTokenOption,
338 pub session_user: Option<SessionUser>,
339 pub nav: Vec<StepNavItem>,
340 pub project_features: &'static [(&'static str, &'static str, &'static str)],
341 }
342
343 /// Full page: item creation wizard.
344 #[derive(Template)]
345 #[template(path = "wizards/wizard_item.html")]
346 pub struct WizardItemTemplate {
347 pub csrf_token: CsrfTokenOption,
348 pub session_user: Option<SessionUser>,
349 pub nav: Vec<StepNavItem>,
350 pub project_slug: String,
351 pub item_type_cards: Vec<(&'static str, &'static str, &'static str)>,
352 }
353
354 // --- Project step partials ---
355
356 /// Wizard step partial: project basics (title, slug, features, category).
357 #[derive(Template)]
358 #[template(path = "wizards/steps/project/basics.html")]
359 pub struct WizardProjectBasicsTemplate {
360 pub nav: Vec<StepNavItem>,
361 pub slug: String,
362 pub project_features: &'static [(&'static str, &'static str, &'static str)],
363 pub title: String,
364 pub features: Vec<String>,
365 pub description: String,
366 pub category_name: String,
367 pub ai_tier: String,
368 pub ai_disclosure: String,
369 }
370
371 /// Wizard step partial: project appearance (cover image upload).
372 #[derive(Template)]
373 #[template(path = "wizards/steps/project/appearance.html")]
374 pub struct WizardProjectAppearanceTemplate {
375 pub nav: Vec<StepNavItem>,
376 pub slug: String,
377 pub project_id: String,
378 pub cover_image_url: Option<String>,
379 pub project_title: String,
380 }
381
382 /// Wizard step partial: project monetization (pricing model, tiers, Stripe).
383 #[derive(Template)]
384 #[template(path = "wizards/steps/project/monetization.html")]
385 pub struct WizardProjectMonetizationTemplate {
386 pub nav: Vec<StepNavItem>,
387 pub slug: String,
388 pub tiers: Vec<WizardTierRow>,
389 pub stripe_connected: bool,
390 /// Current pricing model string value (free, buy_once, pwyw, subscription).
391 pub pricing_model: String,
392 /// Current fixed price in dollars (for buy_once).
393 pub price_dollars: String,
394 /// Current PWYW minimum in dollars.
395 pub pwyw_min_dollars: String,
396 }
397
398 /// Wizard step partial: project first content prompt (item count gate).
399 #[derive(Template)]
400 #[template(path = "wizards/steps/project/first_content.html")]
401 pub struct WizardProjectFirstContentTemplate {
402 pub nav: Vec<StepNavItem>,
403 pub slug: String,
404 pub item_count: u32,
405 }
406
407 /// Wizard step partial: project preview and publish confirmation.
408 #[derive(Template)]
409 #[template(path = "wizards/steps/project/preview.html")]
410 #[allow(dead_code)]
411 pub struct WizardProjectPreviewTemplate {
412 pub csrf_token: CsrfTokenOption,
413 pub nav: Vec<StepNavItem>,
414 pub slug: String,
415 pub title: String,
416 pub features: Vec<String>,
417 pub description: String,
418 pub cover_image_url: Option<String>,
419 pub category_name: Option<String>,
420 pub tier_count: u32,
421 pub item_count: u32,
422 pub tiers: Vec<WizardTierRow>,
423 pub is_public: bool,
424 /// Human-readable pricing display (e.g. "Free", "$9.99", "PWYW").
425 pub pricing_display: String,
426 }
427
428 // --- Item step partials ---
429
430 /// Wizard step partial: item type selection (text, audio, video, software, bundle).
431 #[derive(Template)]
432 #[template(path = "wizards/steps/item/type.html")]
433 pub struct WizardItemTypeTemplate {
434 pub nav: Vec<StepNavItem>,
435 pub project_slug: String,
436 pub item_id: String,
437 pub item_type_cards: Vec<(&'static str, &'static str, &'static str)>,
438 pub selected_type: String,
439 }
440
441 /// Wizard step partial: item basics (title, description, cover image).
442 #[derive(Template)]
443 #[template(path = "wizards/steps/item/basics.html")]
444 pub struct WizardItemBasicsTemplate {
445 pub nav: Vec<StepNavItem>,
446 pub project_slug: String,
447 pub item_id: String,
448 pub title: String,
449 pub description: String,
450 pub cover_image_url: Option<String>,
451 }
452
453 /// Wizard step partial: item content (body editor, bundle picker).
454 #[derive(Template)]
455 #[template(path = "wizards/steps/item/content.html")]
456 pub struct WizardItemContentTemplate {
457 pub nav: Vec<StepNavItem>,
458 pub project_slug: String,
459 pub project_id: String,
460 pub item_id: String,
461 pub item_type: String,
462 pub body: String,
463 /// Non-bundle items in the project available for inclusion.
464 pub bundleable_items: Vec<BundleableItem>,
465 /// IDs of items already selected for this bundle.
466 pub selected_bundle_ids: Vec<String>,
467 /// IDs of items currently marked unlisted.
468 pub unlisted_ids: Vec<String>,
469 }
470
471 /// Lightweight item info for the bundle item picker.
472 pub struct BundleableItem {
473 pub id: String,
474 pub title: String,
475 pub item_type: String,
476 }
477
478 /// Wizard step partial: item sections (reorderable content sections).
479 #[derive(Template)]
480 #[template(path = "wizards/steps/item/sections.html")]
481 pub struct WizardItemSectionsTemplate {
482 pub nav: Vec<StepNavItem>,
483 pub project_slug: String,
484 pub item_id: String,
485 pub sections: Vec<crate::types::ItemSection>,
486 }
487
488 /// Wizard step partial: item pricing (model selection, price entry).
489 #[derive(Template)]
490 #[template(path = "wizards/steps/item/pricing.html")]
491 pub struct WizardItemPricingTemplate {
492 pub nav: Vec<StepNavItem>,
493 pub project_slug: String,
494 pub item_id: String,
495 pub pricing_model: String,
496 pub price_dollars: String,
497 pub pwyw_suggested_dollars: String,
498 pub pwyw_min_dollars: String,
499 }
500
501 /// Wizard step partial: item preview and publish confirmation.
502 #[derive(Template)]
503 #[template(path = "wizards/steps/item/preview.html")]
504 pub struct WizardItemPreviewTemplate {
505 pub csrf_token: CsrfTokenOption,
506 pub nav: Vec<StepNavItem>,
507 pub project_slug: String,
508 pub item_id: String,
509 pub title: String,
510 pub item_type: String,
511 pub description: String,
512 pub price_display: String,
513 pub tag_names: Vec<String>,
514 pub has_content: bool,
515 pub is_public: bool,
516 }
517