Skip to main content

max / mnw-cli

4.2 KB · 141 lines History Blame Raw
1 //! Blog post management screen — list, create, delete posts.
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 super::App;
10 use super::widgets;
11
12 pub fn render(frame: &mut Frame, app: &App) {
13 let area = frame.area();
14
15 let project_title = app
16 .blog_project_title
17 .as_deref()
18 .unwrap_or("Blog");
19
20 let title = Line::from(vec![
21 Span::styled(" Makenot.work ", Style::default().add_modifier(Modifier::BOLD)),
22 Span::raw(" -- "),
23 Span::styled(project_title, Style::default().add_modifier(Modifier::BOLD)),
24 Span::raw(" -- Blog "),
25 ]);
26
27 let block = Block::default()
28 .title(title)
29 .borders(Borders::ALL)
30 .border_style(Style::default().fg(Color::Gray));
31
32 let inner = block.inner(area);
33 frame.render_widget(block, area);
34
35 let chunks = Layout::vertical([
36 Constraint::Length(1), // spacer
37 Constraint::Length(1), // section header
38 Constraint::Min(3), // post list
39 Constraint::Length(1), // status line
40 Constraint::Length(1), // keybindings
41 ])
42 .split(inner);
43
44 // Section header
45 let count = app.blog_posts.len();
46 let header = Paragraph::new(Line::from(vec![
47 Span::raw(" "),
48 Span::styled("Posts", Style::default().add_modifier(Modifier::BOLD)),
49 if count == 0 {
50 Span::raw("")
51 } else {
52 Span::raw(format!(" ({})", count))
53 },
54 ]));
55 frame.render_widget(header, chunks[1]);
56
57 // Post list
58 if app.loading {
59 let loading = Paragraph::new(" Loading...");
60 frame.render_widget(loading, chunks[2]);
61 } else if app.blog_posts.is_empty() {
62 let empty = Paragraph::new(" No blog posts. Press [n] to create one.");
63 frame.render_widget(empty, chunks[2]);
64 } else {
65 render_post_table(frame, app, chunks[2]);
66 }
67
68 // Status line
69 if let Some(ref status) = app.blog_status {
70 let style = if status.starts_with("Error") {
71 Style::default().fg(Color::Red)
72 } else {
73 Style::default().fg(Color::Green)
74 };
75 let status_line = Paragraph::new(Line::from(vec![
76 Span::raw(" "),
77 Span::styled(status.as_str(), style),
78 ]));
79 frame.render_widget(status_line, chunks[3]);
80 }
81
82 // Keybindings
83 let mut key_spans = vec![
84 Span::raw(" "),
85 Span::styled("[j/k]", Style::default().add_modifier(Modifier::BOLD)),
86 Span::raw(" Nav "),
87 Span::styled("[n]", Style::default().add_modifier(Modifier::BOLD)),
88 Span::raw(" New "),
89 ];
90
91 if !app.blog_posts.is_empty() {
92 key_spans.extend([
93 Span::styled("[d]", Style::default().add_modifier(Modifier::BOLD)),
94 Span::raw(" Delete "),
95 ]);
96 }
97
98 key_spans.extend([
99 Span::styled("[r]", Style::default().add_modifier(Modifier::BOLD)),
100 Span::raw(" Refresh "),
101 Span::styled("[Esc]", Style::default().add_modifier(Modifier::BOLD)),
102 Span::raw(" Back"),
103 ]);
104
105 let keys = Paragraph::new(Line::from(key_spans)).style(Style::default().fg(Color::DarkGray));
106 frame.render_widget(keys, chunks[4]);
107 }
108
109 fn render_post_table(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
110 let rows: Vec<Row> = app
111 .blog_posts
112 .iter()
113 .enumerate()
114 .map(|(i, post)| {
115 let status = if post.is_published {
116 "published"
117 } else {
118 "draft"
119 };
120 let date = post.created_at.get(..10).unwrap_or(&post.created_at);
121
122 Row::new(vec![
123 format!(" {}", post.title),
124 post.slug.clone(),
125 status.to_string(),
126 date.to_string(),
127 ])
128 .style(widgets::selected_style(i, Some(app.selected_index)))
129 })
130 .collect();
131
132 let widths = [
133 Constraint::Min(20),
134 Constraint::Length(20),
135 Constraint::Length(10),
136 Constraint::Length(12),
137 ];
138
139 widgets::render_table(frame, area, &[" Title", "Slug", "Status", "Created"], &widths, rows);
140 }
141