Skip to main content

max / makenotwork

8.1 KB · 206 lines History Blame Raw
1 //! blog-screen key-input handler (split out of tui/input.rs).
2
3 use super::{
4 App, AppEvent, BlogCreateStep, ConfirmAction, DataPayload, KeyCode, KeyEvent, MnwApiClient,
5 Screen, load_blog_posts, mpsc,
6 };
7
8 pub(crate) async fn handle_blog_input(
9 key: KeyEvent,
10 app: &mut App,
11 screen: &mut Screen,
12 api: &MnwApiClient,
13 tx: &mpsc::Sender<AppEvent>,
14 ) {
15 // Cancel pending confirmation on any key other than the confirmation key
16 if app.confirm_action.is_some() && !matches!(key.code, KeyCode::Char('d' | 'D')) {
17 app.confirm_action = None;
18 app.blog_status = None;
19 }
20
21 // Creating mode
22 if let Some(step) = app.blog_create_step {
23 // Ctrl+D in body step: advance to schedule
24 if step == BlogCreateStep::Body
25 && key.code == KeyCode::Char('d')
26 && key
27 .modifiers
28 .contains(crossterm::event::KeyModifiers::CONTROL)
29 {
30 app.blog_create_body = app.edit_buffer.clone();
31 app.edit_buffer.clear();
32 app.blog_create_step = Some(BlogCreateStep::Schedule);
33 app.blog_status =
34 Some("Schedule (YYYY-MM-DDTHH:MM:SSZ or Enter for draft):".to_string());
35 return;
36 }
37
38 match key.code {
39 KeyCode::Esc => {
40 app.blog_create_step = None;
41 app.blog_create_title.clear();
42 app.blog_create_body.clear();
43 app.edit_buffer.clear();
44 app.blog_status = None;
45 }
46 KeyCode::Enter => match step {
47 BlogCreateStep::Title => {
48 if !app.edit_buffer.is_empty() {
49 app.blog_create_title = app.edit_buffer.clone();
50 app.edit_buffer.clear();
51 app.blog_create_step = Some(BlogCreateStep::Body);
52 app.blog_status = Some(
53 "Body (markdown, Enter for newline, Ctrl+D when done):".to_string(),
54 );
55 }
56 }
57 BlogCreateStep::Body => {
58 // Enter inserts a newline in body mode
59 app.edit_buffer.push('\n');
60 return;
61 }
62 BlogCreateStep::Schedule => {
63 let title = app.blog_create_title.clone();
64 let body = app.blog_create_body.clone();
65 let schedule_input = app.edit_buffer.trim().to_string();
66 let publish_at = if schedule_input.is_empty() {
67 None
68 } else {
69 Some(schedule_input)
70 };
71
72 if let Screen::Blog(_, project_id) = &*screen {
73 let project_id = project_id.clone();
74 let api = api.clone();
75 let user_id = app.user.user_id.clone();
76 let tx = tx.clone();
77 app.blog_creating = true;
78 let status_msg = if publish_at.is_some() {
79 "Scheduling post..."
80 } else {
81 "Creating draft..."
82 };
83 app.blog_status = Some(status_msg.to_string());
84
85 tokio::spawn(async move {
86 match api
87 .create_blog_post(
88 &user_id,
89 &project_id,
90 &title,
91 &body,
92 false,
93 publish_at.as_deref(),
94 )
95 .await
96 {
97 Ok(_post) => {
98 let _ = tx
99 .send(AppEvent::DataLoaded(DataPayload::BlogCreated))
100 .await;
101 }
102 Err(e) => {
103 let _ = tx
104 .send(AppEvent::DataLoaded(DataPayload::GenericError {
105 error: e.to_string(),
106 }))
107 .await;
108 }
109 }
110 });
111 }
112 app.blog_create_step = None;
113 app.blog_create_title.clear();
114 app.blog_create_body.clear();
115 app.edit_buffer.clear();
116 }
117 },
118 KeyCode::Backspace => {
119 app.edit_buffer.pop();
120 }
121 KeyCode::Char(c) => {
122 app.edit_buffer.push(c);
123 }
124 _ => {}
125 }
126 return;
127 }
128
129 // Normal mode
130 match key.code {
131 KeyCode::Char('j') | KeyCode::Down => app.move_down(screen),
132 KeyCode::Char('k') | KeyCode::Up => app.move_up(screen),
133 KeyCode::Esc => {
134 if let Screen::Blog(pidx, _) = screen {
135 *screen = Screen::Project(*pidx);
136 app.blog_posts.clear();
137 app.blog_status = None;
138 app.selected_index = 0;
139 }
140 }
141 KeyCode::Char('n' | 'N') => {
142 app.blog_create_step = Some(BlogCreateStep::Title);
143 app.edit_buffer.clear();
144 app.blog_status = Some("Title: _".to_string());
145 }
146 KeyCode::Char('d' | 'D') if !app.blog_posts.is_empty() => {
147 let idx = app.selected_index;
148 if matches!(app.confirm_action, Some(ConfirmAction::DeleteBlogPost { post_idx }) if post_idx == idx)
149 {
150 // Confirmed — execute delete
151 app.confirm_action = None;
152 let post_id = app.blog_posts[idx].id.clone();
153 let post_title = app.blog_posts[idx].title.clone();
154 let user_id = app.user.user_id.clone();
155 tracing::info!(
156 user_id = %user_id,
157 post_id = %post_id,
158 post_title = %post_title,
159 "delete blog post confirmed"
160 );
161 let api = api.clone();
162 let tx = tx.clone();
163
164 if let Screen::Blog(_, project_id) = &*screen {
165 let project_id = project_id.clone();
166 app.blog_status = Some("Deleting...".to_string());
167 tokio::spawn(async move {
168 match api.delete_blog_post(&user_id, &post_id).await {
169 Ok(()) => {
170 load_blog_posts(&api, &user_id, &project_id, &tx).await;
171 }
172 Err(e) => {
173 let _ = tx
174 .send(AppEvent::DataLoaded(DataPayload::GenericError {
175 error: e.to_string(),
176 }))
177 .await;
178 }
179 }
180 });
181 }
182 } else {
183 // First press — ask for confirmation
184 app.confirm_action = Some(ConfirmAction::DeleteBlogPost { post_idx: idx });
185 app.blog_status = Some(format!(
186 "Delete '{}'? Press d again to confirm",
187 app.blog_posts[idx].title
188 ));
189 }
190 }
191 KeyCode::Char('r' | 'R') => {
192 if let Screen::Blog(_, project_id) = &*screen {
193 app.loading = true;
194 let project_id = project_id.clone();
195 let api = api.clone();
196 let user_id = app.user.user_id.clone();
197 let tx = tx.clone();
198 tokio::spawn(async move {
199 load_blog_posts(&api, &user_id, &project_id, &tx).await;
200 });
201 }
202 }
203 _ => {}
204 }
205 }
206