Skip to main content

max / mnw-cli

4.5 KB · 145 lines History Blame Raw
1 //! Project detail screen — item list within a project.
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::api::Project;
10 use crate::format;
11
12 use super::App;
13 use super::widgets;
14
15 pub fn render(frame: &mut Frame, app: &App, project: &Project) {
16 let area = frame.area();
17
18 let title = Line::from(vec![
19 Span::styled(" Makenot.work ", Style::default().add_modifier(Modifier::BOLD)),
20 Span::raw(" ── "),
21 Span::styled(&project.title, Style::default().add_modifier(Modifier::BOLD)),
22 Span::raw(" "),
23 ]);
24
25 let block = Block::default()
26 .title(title)
27 .borders(Borders::ALL)
28 .border_style(Style::default().fg(Color::Gray));
29
30 let inner = block.inner(area);
31 frame.render_widget(block, area);
32
33 let chunks = Layout::vertical([
34 Constraint::Length(1), // spacer
35 Constraint::Length(1), // project info line
36 Constraint::Length(1), // spacer
37 Constraint::Length(1), // section header
38 Constraint::Min(3), // item list
39 Constraint::Length(1), // keybindings
40 ])
41 .split(inner);
42
43 // Project info
44 let visibility = if project.is_public {
45 "public"
46 } else {
47 "draft"
48 };
49 let info = Paragraph::new(Line::from(vec![
50 Span::raw(" "),
51 Span::styled(&project.slug, Style::default().fg(Color::DarkGray)),
52 Span::raw(" "),
53 Span::raw(format::format_project_type(&project.project_type)),
54 Span::raw(" "),
55 Span::raw(visibility),
56 Span::raw(" "),
57 Span::raw(format::format_cents(project.revenue_cents)),
58 Span::styled(" revenue", Style::default().fg(Color::DarkGray)),
59 ]));
60 frame.render_widget(info, chunks[1]);
61
62 // Section header
63 let header = Paragraph::new(Line::from(vec![
64 Span::raw(" "),
65 Span::styled("Items", Style::default().add_modifier(Modifier::BOLD)),
66 if app.items.is_empty() {
67 Span::raw("")
68 } else {
69 Span::raw(format!(" ({})", app.items.len()))
70 },
71 ]));
72 frame.render_widget(header, chunks[3]);
73
74 // Item list
75 if app.loading {
76 let loading = Paragraph::new(" Loading...");
77 frame.render_widget(loading, chunks[4]);
78 } else if app.items.is_empty() {
79 let empty = Paragraph::new(" No items in this project.");
80 frame.render_widget(empty, chunks[4]);
81 } else {
82 render_item_table(frame, app, chunks[4]);
83 }
84
85 // Keybindings
86 let mut key_spans = vec![
87 Span::raw(" "),
88 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
89 Span::raw(" Nav "),
90 ];
91
92 if !app.items.is_empty() {
93 key_spans.extend([
94 Span::styled("[Enter]", Style::default().add_modifier(Modifier::BOLD)),
95 Span::raw(" Open "),
96 Span::styled("[p]", Style::default().add_modifier(Modifier::BOLD)),
97 Span::raw(" Pub/Unpub "),
98 Span::styled("[d]", Style::default().add_modifier(Modifier::BOLD)),
99 Span::raw(" Delete "),
100 ]);
101 }
102
103 key_spans.extend([
104 Span::styled("[b]", Style::default().add_modifier(Modifier::BOLD)),
105 Span::raw(" Blog "),
106 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
107 Span::raw(" Refresh "),
108 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
109 Span::raw(" Back"),
110 ]);
111
112 let keys = Paragraph::new(Line::from(key_spans))
113 .style(Style::default().fg(Color::DarkGray));
114 frame.render_widget(keys, chunks[5]);
115 }
116
117 fn render_item_table(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
118 let rows: Vec<Row> = app
119 .items
120 .iter()
121 .enumerate()
122 .map(|(i, item)| {
123 let visibility = if item.is_public { "public" } else { "draft" };
124
125 Row::new(vec![
126 format!(" {}", item.title),
127 format::format_item_type(&item.item_type).to_string(),
128 format::format_price(item.price_cents),
129 visibility.to_string(),
130 ])
131 .style(widgets::selected_style(i, Some(app.selected_index)))
132 })
133 .collect();
134
135 let widths = [
136 Constraint::Min(20),
137 Constraint::Length(12),
138 Constraint::Length(10),
139 Constraint::Length(8),
140 ];
141
142 widgets::render_table(frame, area, &[" Title", "Type", "Price", "Status"], &widths, rows);
143 }
144
145