Skip to main content

max / makenotwork

6.5 KB · 193 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 has_confirm = app.confirm_action.is_some();
34 let has_status = app.item_status.is_some();
35
36 let chunks = Layout::vertical([
37 Constraint::Length(1), // spacer
38 Constraint::Length(1), // project info line
39 Constraint::Length(1), // spacer
40 Constraint::Length(1), // section header
41 Constraint::Min(3), // item list
42 Constraint::Length(if has_confirm || has_status { 1 } else { 0 }), // status/confirm
43 Constraint::Length(1), // keybindings
44 ])
45 .split(inner);
46
47 // Project info
48 let visibility = if project.is_public {
49 "public"
50 } else {
51 "draft"
52 };
53 let info = Paragraph::new(Line::from(vec![
54 Span::raw(" "),
55 Span::styled(&project.slug, Style::default().fg(Color::DarkGray)),
56 Span::raw(" "),
57 Span::raw(format::format_project_type(&project.project_type)),
58 Span::raw(" "),
59 Span::raw(visibility),
60 Span::raw(" "),
61 Span::raw(format::format_cents(project.revenue_cents)),
62 Span::styled(" revenue", Style::default().fg(Color::DarkGray)),
63 ]));
64 frame.render_widget(info, chunks[1]);
65
66 // Section header
67 let header = Paragraph::new(Line::from(vec![
68 Span::raw(" "),
69 Span::styled("Items", Style::default().add_modifier(Modifier::BOLD)),
70 if app.items.is_empty() {
71 Span::raw("")
72 } else {
73 Span::raw(format!(" ({})", app.items.len()))
74 },
75 ]));
76 frame.render_widget(header, chunks[3]);
77
78 // Item list
79 if app.loading {
80 let loading = Paragraph::new(" Loading...");
81 frame.render_widget(loading, chunks[4]);
82 } else if app.items.is_empty() {
83 let empty = Paragraph::new(" No items in this project.");
84 frame.render_widget(empty, chunks[4]);
85 } else {
86 render_item_table(frame, app, chunks[4]);
87 }
88
89 // Status / confirmation line
90 if let Some(ref action) = app.confirm_action {
91 let msg = match action {
92 super::ConfirmAction::BulkPublish { count } => format!(" Publish {} items? [y/n]", count),
93 super::ConfirmAction::BulkUnpublish { count } => format!(" Unpublish {} items? [y/n]", count),
94 super::ConfirmAction::BulkDelete { count } => format!(" Delete {} items? This cannot be undone. [y/n]", count),
95 _ => String::new(),
96 };
97 let confirm = Paragraph::new(msg).style(Style::default().fg(Color::Yellow));
98 frame.render_widget(confirm, chunks[5]);
99 } else if let Some(ref status) = app.item_status {
100 let style = if status.starts_with("Error") {
101 Style::default().fg(Color::Red)
102 } else {
103 Style::default().fg(Color::Green)
104 };
105 let status_line = Paragraph::new(format!(" {}", status)).style(style);
106 frame.render_widget(status_line, chunks[5]);
107 }
108
109 // Keybindings
110 let sel_count = app.selected_items.len();
111 let mut key_spans = vec![
112 Span::raw(" "),
113 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
114 Span::raw(" Nav "),
115 ];
116
117 if !app.items.is_empty() {
118 key_spans.extend([
119 Span::styled("[Space]", Style::default().add_modifier(Modifier::BOLD)),
120 Span::raw(" Select "),
121 Span::styled("[Enter]", Style::default().add_modifier(Modifier::BOLD)),
122 Span::raw(" Open "),
123 Span::styled("[p]", Style::default().add_modifier(Modifier::BOLD)),
124 Span::raw(" Pub/Unpub "),
125 Span::styled("[d]", Style::default().add_modifier(Modifier::BOLD)),
126 Span::raw(" Delete "),
127 ]);
128 }
129
130 if sel_count > 0 {
131 key_spans.extend([
132 Span::styled(
133 format!("{} selected", sel_count),
134 Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
135 ),
136 Span::raw(" "),
137 ]);
138 }
139
140 key_spans.extend([
141 Span::styled("[b]", Style::default().add_modifier(Modifier::BOLD)),
142 Span::raw(" Blog "),
143 Span::styled("[t]", Style::default().add_modifier(Modifier::BOLD)),
144 Span::raw(" Tiers "),
145 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
146 Span::raw(" Refresh "),
147 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
148 Span::raw(" Back"),
149 ]);
150
151 let keys = Paragraph::new(Line::from(key_spans))
152 .style(Style::default().fg(Color::DarkGray));
153 frame.render_widget(keys, chunks[6]);
154 }
155
156 fn render_item_table(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
157 let has_selections = !app.selected_items.is_empty();
158
159 let rows: Vec<Row> = app
160 .items
161 .iter()
162 .enumerate()
163 .map(|(i, item)| {
164 let visibility = if item.is_public { "public" } else { "draft" };
165 let marker = if app.selected_items.contains(&i) {
166 "[x]"
167 } else if has_selections {
168 "[ ]"
169 } else {
170 " "
171 };
172
173 Row::new(vec![
174 format!(" {} {}", marker, item.title),
175 format::format_item_type(&item.item_type).to_string(),
176 format::format_price(item.price_cents),
177 visibility.to_string(),
178 ])
179 .style(widgets::selected_style(i, Some(app.selected_index)))
180 })
181 .collect();
182
183 let widths = [
184 Constraint::Min(20),
185 Constraint::Length(12),
186 Constraint::Length(10),
187 Constraint::Length(8),
188 ];
189
190 widgets::render_table(frame, area, &[" Title", "Type", "Price", "Status"], &widths, rows);
191 }
192
193