//! Authenticated creator dashboard and HTMX tab partials. mod forms; mod main; mod project_tabs; mod tabs; pub mod wizards; use axum::routing::get; use serde::Deserialize; use tower_governor::GovernorLayer; use crate::{constants, csrf::{post_csrf, CsrfRouter}, db, db::Cents, types::*, AppState}; /// Query parameters for analytics time range selection. #[derive(Deserialize)] pub(super) struct AnalyticsQuery { pub range: Option, } /// Convert time-series buckets into chart bar view models. pub(super) fn build_chart_bars(buckets: &[db::analytics::TimeBucket]) -> Vec { let max_revenue = buckets .iter() .map(|b| b.revenue_cents) .max() .unwrap_or(Cents::new(1)) .max(Cents::new(1)); buckets .iter() .map(|b| ChartBar { label: b.label.clone(), height_pct: b.revenue_cents.as_f64() / max_revenue.as_f64() * 100.0, value: format!("${}.{:02}", *b.revenue_cents / 100, *b.revenue_cents % 100), count: b.sales_count, }) .collect() } /// Register dashboard page routes. pub fn dashboard_routes() -> CsrfRouter { let read_rate_limit = crate::helpers::rate_limiter_ms( constants::DASHBOARD_READ_RATE_LIMIT_MS, constants::DASHBOARD_READ_RATE_LIMIT_BURST, ); // Tab endpoints — rate limited to prevent rapid polling let tab_routes = CsrfRouter::new() .route_get("/dashboard/tabs/details", get(tabs::dashboard_tab_details)) .route_get("/dashboard/tabs/settings", get(tabs::dashboard_tab_settings)) .route_get("/dashboard/tabs/profile", get(tabs::dashboard_tab_profile)) .route_get("/dashboard/tabs/account", get(tabs::dashboard_tab_account)) .route_get("/dashboard/tabs/payments", get(tabs::dashboard_tab_payments)) .route_get("/dashboard/tabs/projects", get(tabs::dashboard_tab_projects)) .route_get("/dashboard/tabs/creator", get(tabs::dashboard_tab_creator)) .route_get("/dashboard/tabs/analytics", get(tabs::dashboard_tab_analytics)) .route_get("/dashboard/tabs/synckit", get(tabs::dashboard_tab_synckit)) .route_get("/dashboard/tabs/forums", get(tabs::dashboard_tab_forums)) .route_get("/dashboard/tabs/media", get(tabs::dashboard_tab_media)) .route_get("/dashboard/tabs/ssh-keys", get(tabs::dashboard_tab_ssh_keys)) .route_get("/dashboard/tabs/support", get(tabs::dashboard_tab_support)) .route_get("/dashboard/tabs/contacts", get(tabs::dashboard_tab_contacts)) .route_get("/dashboard/transactions", get(tabs::dashboard_transactions)) .route_get("/dashboard/project/{slug}/tabs/overview", get(project_tabs::project_tab_overview)) .route_get("/dashboard/project/{slug}/tabs/content", get(project_tabs::project_tab_content)) .route_get("/dashboard/project/{slug}/tabs/analytics", get(project_tabs::project_tab_analytics)) .route_get("/dashboard/project/{slug}/tabs/code", get(project_tabs::project_tab_code)) .route_get("/dashboard/project/{slug}/tabs/settings", get(project_tabs::project_tab_settings)) .route_get("/dashboard/project/{slug}/tabs/blog", get(project_tabs::project_tab_blog)) .route_get("/dashboard/project/{slug}/tabs/monetization", get(project_tabs::project_tab_monetization)) .route_get("/dashboard/project/{slug}/tabs/promotions", get(project_tabs::project_tab_promotions)) .route_get("/dashboard/project/{slug}/tabs/subscriptions", get(project_tabs::project_tab_subscriptions)) .route_get("/dashboard/project/{slug}/tabs/members", get(project_tabs::project_tab_members)) .route_get("/dashboard/project/{slug}/tabs/synckit", get(project_tabs::project_tab_synckit)) .route_get("/dashboard/item/{id}/tabs/overview", get(tabs::item_tab_overview)) .route_get("/dashboard/item/{id}/tabs/details", get(tabs::item_tab_details)) .route_get("/dashboard/item/{id}/tabs/pricing", get(tabs::item_tab_pricing)) .route_get("/dashboard/item/{id}/tabs/files", get(tabs::item_tab_files)) .route_get("/dashboard/item/{id}/tabs/sales", get(tabs::item_tab_sales)) .route_get("/dashboard/item/{id}/tabs/embed", get(tabs::item_tab_embed)) .route_get("/dashboard/item/{id}/analytics", get(main::dashboard_item_analytics)) .route_layer(GovernorLayer { config: read_rate_limit }); CsrfRouter::new() .merge(wizards::wizard_routes()) .route_get("/dashboard", get(main::dashboard)) .route_get("/dashboard/project/{slug}", get(main::dashboard_project)) .route_get("/dashboard/item/{id}", get(main::dashboard_item)) .merge(tab_routes) .route_get("/dashboard/item/{id}/edit-row", get(forms::item_edit_row)) .route_get("/dashboard/project/{slug}/blog/new", get(forms::blog_editor)) .route_get("/dashboard/export", get(forms::export_portal)) .route_get("/dashboard/import", get(forms::import_portal)) .route_get("/dashboard/delete-account", get(forms::delete_account_page)) .route("/dashboard/onboarding/dismiss", post_csrf(main::dismiss_onboarding)) .route("/dashboard/onboarding/restore", post_csrf(main::restore_onboarding)) .route("/dashboard/feed/regenerate", post_csrf(tabs::regenerate_feed_url)) } /// Query parameters for filtering dashboard transactions. #[derive(Debug, Deserialize)] #[allow(dead_code)] // Fields populated by query string deserialization pub(super) struct TransactionQuery { pub r#type: Option, pub period: Option, } /// Collect and sort transactions from incoming and outgoing lists. pub(super) fn collect_transactions( incoming_txs: Vec, outgoing_txs: Vec, ) -> Vec { let mut transactions: Vec = Vec::new(); for tx in &incoming_txs { transactions.push(Transaction::from_sale(tx)); } for tx in &outgoing_txs { transactions.push(Transaction::from_purchase(tx)); } transactions.sort_by(|a, b| b.date.cmp(&a.date)); transactions }