Skip to main content

max / makenotwork

8.0 KB · 239 lines History Blame Raw
1 //! Authenticated creator dashboard and HTMX tab partials.
2
3 mod custom_page;
4 mod forms;
5 mod main;
6 mod project_tabs;
7 mod tabs;
8 pub(crate) mod wizards;
9
10 use axum::routing::get;
11 use serde::Deserialize;
12 use tower_governor::GovernorLayer;
13
14 use crate::{
15 AppState, constants,
16 csrf::{CsrfRouter, post_csrf},
17 db,
18 db::Cents,
19 types::{ChartBar, Transaction},
20 };
21
22 /// Query parameters for analytics time range selection.
23 #[derive(Deserialize)]
24 pub(super) struct AnalyticsQuery {
25 pub range: Option<String>,
26 }
27
28 /// Convert time-series buckets into chart bar view models.
29 pub(super) fn build_chart_bars(buckets: &[db::analytics::TimeBucket]) -> Vec<ChartBar> {
30 let max_revenue = buckets
31 .iter()
32 .map(|b| b.revenue_cents)
33 .max()
34 .unwrap_or(Cents::new(1))
35 .max(Cents::new(1));
36 buckets
37 .iter()
38 .map(|b| ChartBar {
39 label: b.label.clone(),
40 height_pct: b.revenue_cents.as_f64() / max_revenue.as_f64() * 100.0,
41 value: format!("${}.{:02}", *b.revenue_cents / 100, *b.revenue_cents % 100),
42 count: b.sales_count,
43 })
44 .collect()
45 }
46
47 /// Register dashboard page routes.
48 pub(crate) fn dashboard_routes() -> CsrfRouter<AppState> {
49 let read_rate_limit = crate::helpers::rate_limiter_ms(
50 constants::DASHBOARD_READ_RATE_LIMIT_MS,
51 constants::DASHBOARD_READ_RATE_LIMIT_BURST,
52 );
53
54 // Tab endpoints, rate limited to prevent rapid polling
55 let tab_routes = CsrfRouter::new()
56 .route_get("/dashboard/tabs/details", get(tabs::dashboard_tab_details))
57 .route_get(
58 "/dashboard/tabs/settings",
59 get(tabs::dashboard_tab_settings),
60 )
61 .route_get("/dashboard/tabs/profile", get(tabs::dashboard_tab_profile))
62 .route_get("/dashboard/tabs/account", get(tabs::dashboard_tab_account))
63 .route_get(
64 "/dashboard/tabs/payments",
65 get(tabs::dashboard_tab_payments),
66 )
67 .route_get(
68 "/dashboard/tabs/projects",
69 get(tabs::dashboard_tab_projects),
70 )
71 .route_get("/dashboard/tabs/creator", get(tabs::dashboard_tab_creator))
72 .route_get(
73 "/dashboard/tabs/analytics",
74 get(tabs::dashboard_tab_analytics),
75 )
76 .route_get("/dashboard/tabs/synckit", get(tabs::dashboard_tab_synckit))
77 .route_get("/dashboard/tabs/forums", get(tabs::dashboard_tab_forums))
78 .route_get("/dashboard/tabs/media", get(tabs::dashboard_tab_media))
79 .route_get(
80 "/dashboard/tabs/ssh-keys",
81 get(tabs::dashboard_tab_ssh_keys),
82 )
83 .route_get("/dashboard/tabs/support", get(tabs::dashboard_tab_support))
84 .route_get(
85 "/dashboard/tabs/contacts",
86 get(tabs::dashboard_tab_contacts),
87 )
88 .route_get(
89 "/dashboard/tabs/payout-summary",
90 get(tabs::dashboard_tab_payout_summary),
91 )
92 .route_get("/dashboard/transactions", get(tabs::dashboard_transactions))
93 .route_get(
94 "/dashboard/project/{slug}/tabs/overview",
95 get(project_tabs::project_tab_overview),
96 )
97 .route_get(
98 "/dashboard/project/{slug}/tabs/content",
99 get(project_tabs::project_tab_content),
100 )
101 .route_get(
102 "/dashboard/project/{slug}/tabs/analytics",
103 get(project_tabs::project_tab_analytics),
104 )
105 .route_get(
106 "/dashboard/project/{slug}/tabs/code",
107 get(project_tabs::project_tab_code),
108 )
109 .route_get(
110 "/dashboard/project/{slug}/tabs/settings",
111 get(project_tabs::project_tab_settings),
112 )
113 .route_get(
114 "/dashboard/project/{slug}/tabs/blog",
115 get(project_tabs::project_tab_blog),
116 )
117 .route_get(
118 "/dashboard/project/{slug}/tabs/monetization",
119 get(project_tabs::project_tab_monetization),
120 )
121 .route_get(
122 "/dashboard/project/{slug}/tabs/promotions",
123 get(project_tabs::project_tab_promotions),
124 )
125 .route_get(
126 "/dashboard/project/{slug}/tabs/subscriptions",
127 get(project_tabs::project_tab_subscriptions),
128 )
129 .route_get(
130 "/dashboard/project/{slug}/tabs/members",
131 get(project_tabs::project_tab_members),
132 )
133 .route_get(
134 "/dashboard/project/{slug}/tabs/synckit",
135 get(project_tabs::project_tab_synckit),
136 )
137 .route_get(
138 "/dashboard/item/{id}/tabs/overview",
139 get(tabs::item_tab_overview),
140 )
141 .route_get(
142 "/dashboard/item/{id}/tabs/details",
143 get(tabs::item_tab_details),
144 )
145 .route_get(
146 "/dashboard/item/{id}/tabs/pricing",
147 get(tabs::item_tab_pricing),
148 )
149 .route_get("/dashboard/item/{id}/tabs/files", get(tabs::item_tab_files))
150 .route_get("/dashboard/item/{id}/tabs/sales", get(tabs::item_tab_sales))
151 .route_get("/dashboard/item/{id}/tabs/embed", get(tabs::item_tab_embed))
152 .route_get(
153 "/dashboard/item/{id}/analytics",
154 get(main::dashboard_item_analytics),
155 )
156 .route_layer(GovernorLayer::new(read_rate_limit));
157
158 CsrfRouter::new()
159 .merge(wizards::wizard_routes())
160 .route_get("/dashboard", get(main::dashboard))
161 .route_get("/dashboard/project/{slug}", get(main::dashboard_project))
162 .route_get("/dashboard/item/{id}", get(main::dashboard_item))
163 .merge(tab_routes)
164 .route_get("/dashboard/item/{id}/edit-row", get(forms::item_edit_row))
165 .route_get(
166 "/dashboard/project/{slug}/blog/new",
167 get(forms::blog_editor),
168 )
169 .route_get("/dashboard/export", get(forms::export_portal))
170 .route_get("/dashboard/import", get(forms::import_portal))
171 .route_get("/dashboard/delete-account", get(forms::delete_account_page))
172 .route(
173 "/dashboard/onboarding/dismiss",
174 post_csrf(main::dismiss_onboarding),
175 )
176 .route(
177 "/dashboard/onboarding/restore",
178 post_csrf(main::restore_onboarding),
179 )
180 .route(
181 "/dashboard/feed/regenerate",
182 post_csrf(tabs::regenerate_feed_url),
183 )
184 // Custom-page editors (profile + per project).
185 .route_get("/dashboard/custom-page", get(custom_page::user_editor))
186 .route("/dashboard/custom-page", post_csrf(custom_page::user_save))
187 .route(
188 "/dashboard/custom-page/draft",
189 post_csrf(custom_page::user_autosave),
190 )
191 .route(
192 "/dashboard/custom-page/reset",
193 post_csrf(custom_page::user_reset),
194 )
195 .route_get(
196 "/dashboard/project/{slug}/custom-page",
197 get(custom_page::project_editor),
198 )
199 .route(
200 "/dashboard/project/{slug}/custom-page",
201 post_csrf(custom_page::project_save),
202 )
203 .route(
204 "/dashboard/project/{slug}/custom-page/draft",
205 post_csrf(custom_page::project_autosave),
206 )
207 .route(
208 "/dashboard/project/{slug}/custom-page/reset",
209 post_csrf(custom_page::project_reset),
210 )
211 }
212
213 /// Query parameters for filtering dashboard transactions.
214 #[derive(Debug, Deserialize)]
215 #[allow(dead_code)] // Fields populated by query string deserialization
216 pub(super) struct TransactionQuery {
217 pub r#type: Option<String>,
218 pub period: Option<String>,
219 }
220
221 /// Collect and sort transactions from incoming and outgoing lists.
222 pub(super) fn collect_transactions(
223 incoming_txs: &[db::DbTransaction],
224 outgoing_txs: &[db::DbTransaction],
225 ) -> Vec<Transaction> {
226 let mut transactions: Vec<Transaction> = Vec::new();
227
228 for tx in incoming_txs {
229 transactions.push(Transaction::from_sale(tx));
230 }
231
232 for tx in outgoing_txs {
233 transactions.push(Transaction::from_purchase(tx));
234 }
235
236 transactions.sort_by(|a, b| b.date.cmp(&a.date));
237 transactions
238 }
239