Skip to main content

max / makenotwork

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