//! Authenticated creator dashboard and HTMX tab partials. mod custom_page; mod forms; mod main; mod project_tabs; mod tabs; pub(crate) mod wizards; use axum::routing::get; use serde::Deserialize; use tower_governor::GovernorLayer; use crate::{ AppState, constants, csrf::{CsrfRouter, post_csrf}, db, db::Cents, types::{ChartBar, Transaction}, }; /// 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], currency: crate::currency::SettlementCurrency, ) -> 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: crate::formatting::format_revenue(*b.revenue_cents, currency), count: b.sales_count, }) .collect() } /// Register dashboard page routes. pub(crate) 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/tabs/payout-summary", get(tabs::dashboard_tab_payout_summary), ) .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::new(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), ) // Custom-page editors (profile + per project). .route_get("/dashboard/custom-page", get(custom_page::user_editor)) .route("/dashboard/custom-page", post_csrf(custom_page::user_save)) .route( "/dashboard/custom-page/draft", post_csrf(custom_page::user_autosave), ) .route( "/dashboard/custom-page/reset", post_csrf(custom_page::user_reset), ) .route_get( "/dashboard/project/{slug}/custom-page", get(custom_page::project_editor), ) .route( "/dashboard/project/{slug}/custom-page", post_csrf(custom_page::project_save), ) .route( "/dashboard/project/{slug}/custom-page/draft", post_csrf(custom_page::project_autosave), ) .route( "/dashboard/project/{slug}/custom-page/reset", post_csrf(custom_page::project_reset), ) } /// 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: &[db::DbTransaction], outgoing_txs: &[db::DbTransaction], ) -> 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 }