Skip to main content

max / makenotwork

5.6 KB · 179 lines History Blame Raw
1 //! Home screen — project list with stats overview.
2
3 use ratatui::Frame;
4 use ratatui::layout::{Constraint, Layout};
5 use ratatui::style::{Color, Modifier, Style};
6 use ratatui::text::{Line, Span};
7 use ratatui::widgets::{Block, Borders, Paragraph, Row};
8
9 use crate::format;
10
11 use super::App;
12 use super::widgets;
13
14 pub fn render(frame: &mut Frame, app: &App) {
15 let area = frame.area();
16
17 let tier_label = app
18 .user
19 .creator_tier
20 .as_deref()
21 .map(format::format_tier)
22 .unwrap_or("No tier");
23
24 let title = Line::from(vec![
25 Span::styled(" Makenot.work ", Style::default().add_modifier(Modifier::BOLD)),
26 Span::raw(" ── "),
27 Span::styled(
28 &app.user.username,
29 Style::default().add_modifier(Modifier::BOLD),
30 ),
31 Span::raw(" ── "),
32 Span::raw(tier_label),
33 Span::raw(" "),
34 ]);
35
36 let block = Block::default()
37 .title(title)
38 .borders(Borders::ALL)
39 .border_style(Style::default().fg(Color::Gray));
40
41 let inner = block.inner(area);
42 frame.render_widget(block, area);
43
44 let chunks = Layout::vertical([
45 Constraint::Length(1), // spacer
46 Constraint::Length(3), // stats bar
47 Constraint::Length(1), // spacer
48 Constraint::Length(1), // section header
49 Constraint::Min(3), // project list
50 Constraint::Length(1), // keybindings
51 ])
52 .split(inner);
53
54 // Stats bar
55 render_stats(frame, app, chunks[1]);
56
57 // Section header
58 let header = Paragraph::new(Line::from(vec![
59 Span::raw(" "),
60 Span::styled("Projects", Style::default().add_modifier(Modifier::BOLD)),
61 if app.projects.is_empty() {
62 Span::raw("")
63 } else {
64 Span::raw(format!(" ({})", app.projects.len()))
65 },
66 ]));
67 frame.render_widget(header, chunks[3]);
68
69 // Project list
70 if app.loading {
71 let loading = Paragraph::new(" Loading...");
72 frame.render_widget(loading, chunks[4]);
73 } else if app.projects.is_empty() {
74 let empty = Paragraph::new(" No projects yet. Create one at makenot.work/dashboard");
75 frame.render_widget(empty, chunks[4]);
76 } else {
77 render_project_table(frame, app, chunks[4]);
78 }
79
80 // Keybindings
81 let keys = Paragraph::new(Line::from(vec![
82 Span::raw(" "),
83 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
84 Span::raw(" Nav "),
85 Span::styled("[Enter]", Style::default().add_modifier(Modifier::BOLD)),
86 Span::raw(" Open "),
87 Span::styled("[u]", Style::default().add_modifier(Modifier::BOLD)),
88 Span::raw(" Upload "),
89 Span::styled("[a]", Style::default().add_modifier(Modifier::BOLD)),
90 Span::raw(" Analytics "),
91 Span::styled("[p]", Style::default().add_modifier(Modifier::BOLD)),
92 Span::raw(" Promo "),
93 Span::styled("[c]", Style::default().add_modifier(Modifier::BOLD)),
94 Span::raw(" Collections "),
95 Span::styled("[s]", Style::default().add_modifier(Modifier::BOLD)),
96 Span::raw(" Settings "),
97 Span::styled("[q]", Style::default().add_modifier(Modifier::BOLD)),
98 Span::raw(" Quit"),
99 ]))
100 .style(Style::default().fg(Color::DarkGray));
101 frame.render_widget(keys, chunks[5]);
102 }
103
104 fn render_stats(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
105 let stats_chunks = Layout::horizontal([
106 Constraint::Ratio(1, 4),
107 Constraint::Ratio(1, 4),
108 Constraint::Ratio(1, 4),
109 Constraint::Ratio(1, 4),
110 ])
111 .split(area);
112
113 let (revenue, sales, followers, items) = if let Some(ref s) = app.stats {
114 (
115 format::format_cents(s.current_revenue_cents),
116 s.current_sales.to_string(),
117 s.current_followers.to_string(),
118 s.total_items.to_string(),
119 )
120 } else {
121 ("--".into(), "--".into(), "--".into(), "--".into())
122 };
123
124 let stat_items = [
125 ("Revenue", &revenue),
126 ("Sales", &sales),
127 ("Followers", &followers),
128 ("Items", &items),
129 ];
130
131 for (i, (label, value)) in stat_items.iter().enumerate() {
132 let block = Block::default()
133 .borders(Borders::ALL)
134 .border_style(Style::default().fg(Color::DarkGray));
135 let inner = block.inner(stats_chunks[i]);
136 frame.render_widget(block, stats_chunks[i]);
137
138 let text = Paragraph::new(Line::from(vec![
139 Span::styled(
140 format!(" {value}"),
141 Style::default().add_modifier(Modifier::BOLD),
142 ),
143 Span::styled(format!(" {label}"), Style::default().fg(Color::DarkGray)),
144 ]));
145 frame.render_widget(text, inner);
146 }
147 }
148
149 fn render_project_table(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
150 let rows: Vec<Row> = app
151 .projects
152 .iter()
153 .enumerate()
154 .map(|(i, p)| {
155 let visibility = if p.is_public { "public" } else { "draft" };
156
157 Row::new(vec![
158 format!(" {}", p.title),
159 format::format_project_type(&p.project_type).to_string(),
160 visibility.to_string(),
161 p.item_count.to_string(),
162 format::format_cents(p.revenue_cents),
163 ])
164 .style(widgets::selected_style(i, Some(app.selected_index)))
165 })
166 .collect();
167
168 let widths = [
169 Constraint::Min(20),
170 Constraint::Length(12),
171 Constraint::Length(8),
172 Constraint::Length(7),
173 Constraint::Length(12),
174 ];
175
176 widgets::render_table(frame, area, &[" Title", "Type", "Status", "Items", "Revenue"], &widths, rows);
177 }
178
179