Skip to main content

max / makenotwork

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