//! Home screen — project list with stats overview. use makeover_tui::makeover_layout::{Column, Priority, Width}; use makeover_tui::table::{self, Cell, Sizing, TableStyle}; use ratatui::Frame; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, TableState}; use crate::format; use super::App; /// The project list's columns. /// /// Title is the only one without which a row stops saying which project it is, /// so it is the only [`Priority::Essential`] one. Revenue and status are what /// this screen is opened for, which puts them above the type and the count when /// the window narrows. const PROJECT_COLUMNS: [Column<'static>; 5] = [ Column { name: "Title", width: Width::Fill, priority: Priority::Essential, sortable: false, sorted: None, }, Column { name: "Type", width: Width::Fixed, priority: Priority::Optional, sortable: false, sorted: None, }, Column { name: "Status", width: Width::Fixed, priority: Priority::Secondary, sortable: false, sorted: None, }, Column { name: "Items", width: Width::Fixed, priority: Priority::Optional, sortable: false, sorted: None, }, Column { name: "Revenue", width: Width::Fixed, priority: Priority::Secondary, sortable: false, sorted: None, }, ]; /// The tracks the hand-written `Constraint`s carried, lifted rather than /// re-chosen. Revenue is wider than the amount alone needs: a three-character /// symbol (`CA$`) plus the `+N` multi-currency marker has to fit without /// truncating. const PROJECT_SIZING: Sizing<'static> = Sizing { lengths: &[ ("Title", 20), ("Type", 12), ("Status", 8), ("Items", 7), ("Revenue", 15), ], fallback: 8, }; pub(crate) fn render(frame: &mut Frame, app: &App) { let area = frame.area(); let tier_label = app .user .creator_tier .as_deref() .map_or("No tier", format::format_tier); let title = Line::from(vec![ Span::styled( " Makenot.work ", Style::default().add_modifier(Modifier::BOLD), ), Span::raw(" ── "), Span::styled( &app.user.username, Style::default().add_modifier(Modifier::BOLD), ), Span::raw(" ── "), Span::raw(tier_label), Span::raw(" "), ]); let block = Block::default() .title(title) .borders(Borders::ALL) .border_style(Style::default().fg(app.theme.line_border)); let inner = block.inner(area); frame.render_widget(block, area); let chunks = Layout::vertical([ Constraint::Length(1), // spacer Constraint::Length(3), // stats bar Constraint::Length(1), // spacer Constraint::Length(1), // section header Constraint::Min(3), // project list Constraint::Length(1), // keybindings ]) .split(inner); // Stats bar render_stats(frame, app, chunks[1]); // Section header let header = Paragraph::new(Line::from(vec![ Span::raw(" "), Span::styled("Projects", Style::default().add_modifier(Modifier::BOLD)), if app.projects.is_empty() { Span::raw("") } else { Span::raw(format!(" ({})", app.projects.len())) }, ])); frame.render_widget(header, chunks[3]); // Project list if app.loading { let loading = Paragraph::new(" Loading..."); frame.render_widget(loading, chunks[4]); } else if app.projects.is_empty() { let empty = Paragraph::new(" No projects yet. Create one at makenot.work/dashboard"); frame.render_widget(empty, chunks[4]); } else { render_project_table(frame, app, chunks[4]); } // Keybindings let keys = Paragraph::new(Line::from(vec![ Span::raw(" "), Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Nav "), Span::styled("[Enter]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Open "), Span::styled("[u]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Upload "), Span::styled("[a]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Analytics "), Span::styled("[p]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Promo "), Span::styled("[c]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Collections "), Span::styled("[s]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Settings "), Span::styled("[q]", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" Quit"), ])) .style(Style::default().fg(app.theme.content_muted)); frame.render_widget(keys, chunks[5]); } fn render_stats(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) { let stats_chunks = Layout::horizontal([ Constraint::Ratio(1, 4), Constraint::Ratio(1, 4), Constraint::Ratio(1, 4), Constraint::Ratio(1, 4), ]) .split(area); let (revenue, sales, followers, items) = if let Some(ref s) = app.stats { ( format::format_cents(s.current_revenue_cents, app.currency()), s.current_sales.to_string(), s.current_followers.to_string(), s.total_items.to_string(), ) } else { ("--".into(), "--".into(), "--".into(), "--".into()) }; let stat_items = [ ("Revenue", &revenue), ("Sales", &sales), ("Followers", &followers), ("Items", &items), ]; for (i, (label, value)) in stat_items.iter().enumerate() { let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(app.theme.content_muted)); let inner = block.inner(stats_chunks[i]); frame.render_widget(block, stats_chunks[i]); let text = Paragraph::new(Line::from(vec![ Span::styled( format!(" {value}"), Style::default().add_modifier(Modifier::BOLD), ), Span::styled( format!(" {label}"), Style::default().fg(app.theme.content_muted), ), ])); frame.render_widget(text, inner); } } fn render_project_table(frame: &mut Frame, app: &App, area: Rect) { let area = super::indent(area, 2); let rows: Vec> = app .projects .iter() .map(|p| { vec![ Cell::new("Title", p.title.as_str()), Cell::new("Type", format::format_project_type(&p.project_type)), Cell::new("Status", if p.is_public { "public" } else { "draft" }), Cell::new("Items", p.item_count.to_string()), Cell::new("Revenue", p.revenue().display_compact(app.currency())), ] }) .collect(); let style = TableStyle::from_theme(&app.theme); let table = table::table(&PROJECT_COLUMNS, &rows, &PROJECT_SIZING, &style, area.width); let mut state = TableState::default().with_selected(Some(app.selected_index)); frame.render_stateful_widget(table, area, &mut state); }