Skip to main content

max / makenotwork

6.0 KB · 126 lines History Blame Raw
1 //! Authenticated creator dashboard and HTMX tab partials.
2
3 mod forms;
4 mod main;
5 mod project_tabs;
6 mod tabs;
7 pub mod wizards;
8
9 use axum::routing::get;
10 use serde::Deserialize;
11 use tower_governor::GovernorLayer;
12
13 use crate::{constants, csrf::{post_csrf, CsrfRouter}, db, db::Cents, types::*, AppState};
14
15 /// Query parameters for analytics time range selection.
16 #[derive(Deserialize)]
17 pub(super) struct AnalyticsQuery {
18 pub range: Option<String>,
19 }
20
21 /// Convert time-series buckets into chart bar view models.
22 pub(super) fn build_chart_bars(buckets: &[db::analytics::TimeBucket]) -> Vec<ChartBar> {
23 let max_revenue = buckets
24 .iter()
25 .map(|b| b.revenue_cents)
26 .max()
27 .unwrap_or(Cents::new(1))
28 .max(Cents::new(1));
29 buckets
30 .iter()
31 .map(|b| ChartBar {
32 label: b.label.clone(),
33 height_pct: b.revenue_cents.as_f64() / max_revenue.as_f64() * 100.0,
34 value: format!("${}.{:02}", *b.revenue_cents / 100, *b.revenue_cents % 100),
35 count: b.sales_count,
36 })
37 .collect()
38 }
39
40 /// Register dashboard page routes.
41 pub fn dashboard_routes() -> CsrfRouter<AppState> {
42 let read_rate_limit = crate::helpers::rate_limiter_ms(
43 constants::DASHBOARD_READ_RATE_LIMIT_MS,
44 constants::DASHBOARD_READ_RATE_LIMIT_BURST,
45 );
46
47 // Tab endpoints — rate limited to prevent rapid polling
48 let tab_routes = CsrfRouter::new()
49 .route_get("/dashboard/tabs/details", get(tabs::dashboard_tab_details))
50 .route_get("/dashboard/tabs/settings", get(tabs::dashboard_tab_settings))
51 .route_get("/dashboard/tabs/profile", get(tabs::dashboard_tab_profile))
52 .route_get("/dashboard/tabs/account", get(tabs::dashboard_tab_account))
53 .route_get("/dashboard/tabs/payments", get(tabs::dashboard_tab_payments))
54 .route_get("/dashboard/tabs/projects", get(tabs::dashboard_tab_projects))
55 .route_get("/dashboard/tabs/creator", get(tabs::dashboard_tab_creator))
56 .route_get("/dashboard/tabs/analytics", get(tabs::dashboard_tab_analytics))
57 .route_get("/dashboard/tabs/synckit", get(tabs::dashboard_tab_synckit))
58 .route_get("/dashboard/tabs/forums", get(tabs::dashboard_tab_forums))
59 .route_get("/dashboard/tabs/media", get(tabs::dashboard_tab_media))
60 .route_get("/dashboard/tabs/ssh-keys", get(tabs::dashboard_tab_ssh_keys))
61 .route_get("/dashboard/tabs/support", get(tabs::dashboard_tab_support))
62 .route_get("/dashboard/tabs/contacts", get(tabs::dashboard_tab_contacts))
63 .route_get("/dashboard/transactions", get(tabs::dashboard_transactions))
64 .route_get("/dashboard/project/{slug}/tabs/overview", get(project_tabs::project_tab_overview))
65 .route_get("/dashboard/project/{slug}/tabs/content", get(project_tabs::project_tab_content))
66 .route_get("/dashboard/project/{slug}/tabs/analytics", get(project_tabs::project_tab_analytics))
67 .route_get("/dashboard/project/{slug}/tabs/code", get(project_tabs::project_tab_code))
68 .route_get("/dashboard/project/{slug}/tabs/settings", get(project_tabs::project_tab_settings))
69 .route_get("/dashboard/project/{slug}/tabs/blog", get(project_tabs::project_tab_blog))
70 .route_get("/dashboard/project/{slug}/tabs/monetization", get(project_tabs::project_tab_monetization))
71 .route_get("/dashboard/project/{slug}/tabs/promotions", get(project_tabs::project_tab_promotions))
72 .route_get("/dashboard/project/{slug}/tabs/subscriptions", get(project_tabs::project_tab_subscriptions))
73 .route_get("/dashboard/project/{slug}/tabs/members", get(project_tabs::project_tab_members))
74 .route_get("/dashboard/project/{slug}/tabs/synckit", get(project_tabs::project_tab_synckit))
75 .route_get("/dashboard/item/{id}/tabs/overview", get(tabs::item_tab_overview))
76 .route_get("/dashboard/item/{id}/tabs/details", get(tabs::item_tab_details))
77 .route_get("/dashboard/item/{id}/tabs/pricing", get(tabs::item_tab_pricing))
78 .route_get("/dashboard/item/{id}/tabs/files", get(tabs::item_tab_files))
79 .route_get("/dashboard/item/{id}/tabs/sales", get(tabs::item_tab_sales))
80 .route_get("/dashboard/item/{id}/tabs/embed", get(tabs::item_tab_embed))
81 .route_get("/dashboard/item/{id}/analytics", get(main::dashboard_item_analytics))
82 .route_layer(GovernorLayer { config: read_rate_limit });
83
84 CsrfRouter::new()
85 .merge(wizards::wizard_routes())
86 .route_get("/dashboard", get(main::dashboard))
87 .route_get("/dashboard/project/{slug}", get(main::dashboard_project))
88 .route_get("/dashboard/item/{id}", get(main::dashboard_item))
89 .merge(tab_routes)
90 .route_get("/dashboard/item/{id}/edit-row", get(forms::item_edit_row))
91 .route_get("/dashboard/project/{slug}/blog/new", get(forms::blog_editor))
92 .route_get("/dashboard/export", get(forms::export_portal))
93 .route_get("/dashboard/import", get(forms::import_portal))
94 .route_get("/dashboard/delete-account", get(forms::delete_account_page))
95 .route("/dashboard/onboarding/dismiss", post_csrf(main::dismiss_onboarding))
96 .route("/dashboard/onboarding/restore", post_csrf(main::restore_onboarding))
97 .route("/dashboard/feed/regenerate", post_csrf(tabs::regenerate_feed_url))
98 }
99
100 /// Query parameters for filtering dashboard transactions.
101 #[derive(Debug, Deserialize)]
102 #[allow(dead_code)] // Fields populated by query string deserialization
103 pub(super) struct TransactionQuery {
104 pub r#type: Option<String>,
105 pub period: Option<String>,
106 }
107
108 /// Collect and sort transactions from incoming and outgoing lists.
109 pub(super) fn collect_transactions(
110 incoming_txs: Vec<db::DbTransaction>,
111 outgoing_txs: Vec<db::DbTransaction>,
112 ) -> Vec<Transaction> {
113 let mut transactions: Vec<Transaction> = Vec::new();
114
115 for tx in &incoming_txs {
116 transactions.push(Transaction::from_sale(tx));
117 }
118
119 for tx in &outgoing_txs {
120 transactions.push(Transaction::from_purchase(tx));
121 }
122
123 transactions.sort_by(|a, b| b.date.cmp(&a.date));
124 transactions
125 }
126