rust_kanban/ui/rendering/view/
reset_password.rs

1use crate::{
2    app::{
3        state::{AppStatus, Focus, KeyBindingEnum},
4        App,
5    },
6    constants::{HIDDEN_PASSWORD_SYMBOL, MIN_TIME_BETWEEN_SENDING_RESET_LINK},
7    ui::{
8        rendering::{
9            common::{
10                draw_crab_pattern, draw_title, render_blank_styled_canvas_with_margin,
11                render_close_button,
12            },
13            utils::{
14                calculate_viewport_corrected_cursor_position, centered_rect_with_length,
15                check_if_active_and_get_style, check_if_mouse_is_in_area,
16                get_mouse_focusable_field_style,
17            },
18            view::ResetPassword,
19        },
20        Renderable,
21    },
22};
23use ratatui::{
24    layout::{Alignment, Constraint, Direction, Layout},
25    text::{Line, Span},
26    widgets::{Block, BorderType, Borders, Clear, Paragraph},
27    Frame,
28};
29use std::time::Duration;
30
31impl Renderable for ResetPassword {
32    fn render(rect: &mut Frame, app: &mut App, is_active: bool) {
33        if is_active {
34            if app.state.focus == Focus::EmailIDField
35                || app.state.focus == Focus::ResetPasswordLinkField
36                || app.state.focus == Focus::PasswordField
37                || app.state.focus == Focus::ConfirmPasswordField
38            {
39                if app.state.app_status != AppStatus::UserInput {
40                    app.state.app_status = AppStatus::UserInput;
41                }
42            } else if app.state.app_status != AppStatus::Initialized {
43                app.state.app_status = AppStatus::Initialized;
44            }
45        }
46
47        let main_chunks = Layout::default()
48            .direction(Direction::Vertical)
49            .constraints([Constraint::Length(3), Constraint::Fill(1)].as_ref())
50            .split(rect.area());
51
52        let chunks = Layout::default()
53            .direction(Direction::Horizontal)
54            .constraints([
55                Constraint::Fill(1),
56                Constraint::Length(2),
57                Constraint::Length(50),
58            ])
59            .split(main_chunks[1]);
60
61        let info_box = centered_rect_with_length(54, 13, chunks[0]);
62
63        let info_chunks = Layout::default()
64            .direction(Direction::Vertical)
65            .constraints(
66                [
67                    Constraint::Length(1),
68                    Constraint::Length(1),
69                    Constraint::Fill(1),
70                ]
71                .as_ref(),
72            )
73            .margin(1)
74            .split(info_box);
75
76        let form_chunks = Layout::default()
77            .direction(Direction::Vertical)
78            .constraints([
79                Constraint::Length((chunks[2].height - 24) / 2),
80                Constraint::Length(3),
81                Constraint::Length(3),
82                Constraint::Length(1),
83                Constraint::Length(3),
84                Constraint::Length(3),
85                Constraint::Length(3),
86                Constraint::Length(3),
87                Constraint::Length(3),
88                Constraint::Length((chunks[2].height - 24) / 2),
89            ])
90            .margin(1)
91            .split(chunks[2]);
92
93        let email_id_chunk = form_chunks[1];
94        let send_reset_link_button_chunk = form_chunks[2];
95        let reset_link_chunk = form_chunks[4];
96        let new_password_chunk = form_chunks[5];
97        let confirm_new_password_chunk = form_chunks[6];
98        let show_password_main_chunk = form_chunks[7];
99        let submit_button_chunk = form_chunks[8];
100
101        let show_password_chunks = Layout::default()
102            .direction(Direction::Horizontal)
103            .constraints([
104                Constraint::Length(show_password_main_chunk.width - 7),
105                Constraint::Length(5),
106            ])
107            .margin(1)
108            .split(show_password_main_chunk);
109
110        let submit_button_chunks = Layout::default()
111            .direction(Direction::Horizontal)
112            .constraints([
113                Constraint::Length((submit_button_chunk.width - 12) / 2),
114                Constraint::Length(12),
115                Constraint::Length((submit_button_chunk.width - 12) / 2),
116            ])
117            .split(submit_button_chunk);
118
119        let email_id_field_style = get_mouse_focusable_field_style(
120            app,
121            Focus::EmailIDField,
122            &email_id_chunk,
123            is_active,
124            true,
125        );
126
127        let send_reset_link_button_style = if !is_active {
128            app.current_theme.inactive_text_style
129        } else if let Some(last_reset_password_link_sent_time) =
130            app.state.last_reset_password_link_sent_time
131        {
132            if last_reset_password_link_sent_time.elapsed()
133                < Duration::from_secs(MIN_TIME_BETWEEN_SENDING_RESET_LINK)
134            {
135                app.current_theme.inactive_text_style
136            } else if check_if_mouse_is_in_area(
137                &app.state.current_mouse_coordinates,
138                &send_reset_link_button_chunk,
139            ) {
140                if app.state.mouse_focus != Some(Focus::SendResetPasswordLinkButton) {
141                    app.state.app_status = AppStatus::Initialized;
142                } else {
143                    app.state.app_status = AppStatus::UserInput;
144                }
145                app.state.mouse_focus = Some(Focus::SendResetPasswordLinkButton);
146                app.state.set_focus(Focus::SendResetPasswordLinkButton);
147                app.current_theme.mouse_focus_style
148            } else if app.state.focus == Focus::SendResetPasswordLinkButton {
149                app.current_theme.keyboard_focus_style
150            } else {
151                app.current_theme.general_style
152            }
153        } else if check_if_mouse_is_in_area(
154            &app.state.current_mouse_coordinates,
155            &send_reset_link_button_chunk,
156        ) {
157            if app.state.mouse_focus != Some(Focus::SendResetPasswordLinkButton) {
158                app.state.app_status = AppStatus::Initialized;
159            } else {
160                app.state.app_status = AppStatus::UserInput;
161            }
162            app.state.mouse_focus = Some(Focus::SendResetPasswordLinkButton);
163            app.state.set_focus(Focus::SendResetPasswordLinkButton);
164            app.current_theme.mouse_focus_style
165        } else if app.state.focus == Focus::SendResetPasswordLinkButton {
166            app.current_theme.keyboard_focus_style
167        } else {
168            app.current_theme.general_style
169        };
170
171        let separator_style = check_if_active_and_get_style(
172            is_active,
173            app.current_theme.inactive_text_style,
174            app.current_theme.general_style,
175        );
176
177        let reset_link_field_style = get_mouse_focusable_field_style(
178            app,
179            Focus::ResetPasswordLinkField,
180            &reset_link_chunk,
181            is_active,
182            true,
183        );
184
185        let new_password_field_style = get_mouse_focusable_field_style(
186            app,
187            Focus::PasswordField,
188            &new_password_chunk,
189            is_active,
190            true,
191        );
192
193        let confirm_new_password_field_style = get_mouse_focusable_field_style(
194            app,
195            Focus::ConfirmPasswordField,
196            &confirm_new_password_chunk,
197            is_active,
198            true,
199        );
200
201        let show_password_style = get_mouse_focusable_field_style(
202            app,
203            Focus::ExtraFocus,
204            &show_password_main_chunk,
205            is_active,
206            false,
207        );
208
209        let submit_button_style = get_mouse_focusable_field_style(
210            app,
211            Focus::SubmitButton,
212            &submit_button_chunks[1],
213            is_active,
214            false,
215        );
216
217        let general_style = check_if_active_and_get_style(
218            is_active,
219            app.current_theme.inactive_text_style,
220            app.current_theme.general_style,
221        );
222        let help_key_style = check_if_active_and_get_style(
223            is_active,
224            app.current_theme.inactive_text_style,
225            app.current_theme.help_key_style,
226        );
227        let help_text_style = check_if_active_and_get_style(
228            is_active,
229            app.current_theme.inactive_text_style,
230            app.current_theme.help_text_style,
231        );
232
233        let crab_paragraph = draw_crab_pattern(
234            chunks[0],
235            app.current_theme.inactive_text_style,
236            is_active,
237            app.config.disable_animations,
238        );
239
240        let info_border = Block::default()
241            .borders(Borders::ALL)
242            .border_type(BorderType::Rounded)
243            .border_style(separator_style);
244
245        let info_header = Paragraph::new("Reset Password")
246            .style(general_style)
247            .block(Block::default())
248            .alignment(Alignment::Center);
249
250        let accept_key = app
251            .get_first_keybinding(KeyBindingEnum::Accept)
252            .unwrap_or("".to_string());
253        let next_focus_key = app
254            .get_first_keybinding(KeyBindingEnum::NextFocus)
255            .unwrap_or("".to_string());
256        let prv_focus_key = app
257            .get_first_keybinding(KeyBindingEnum::PrvFocus)
258            .unwrap_or("".to_string());
259
260        let help_lines = vec![
261            Line::from(Span::styled(
262                "1) Enter your email and send reset link first.",
263                help_text_style,
264            )),
265            Line::from(Span::styled(
266                "2) Copy the reset link from your email and then paste the reset link.",
267                help_text_style,
268            )),
269            Line::from(Span::styled(
270                "3) Enter new password and confirm the new password.",
271                help_text_style,
272            )),
273            Line::from(""),
274            Line::from(Span::styled(
275                "### Check Spam folder if you don't see the email ###",
276                help_text_style,
277            )),
278            Line::from(""),
279            Line::from(vec![
280                Span::styled("Press ", help_text_style),
281                Span::styled(next_focus_key, help_key_style),
282                Span::styled(" or ", help_text_style),
283                Span::styled(prv_focus_key, help_key_style),
284                Span::styled(" to change focus. Press ", help_text_style),
285                Span::styled(accept_key, help_key_style),
286                Span::styled(" to submit.", help_text_style),
287            ]),
288        ];
289
290        let help_paragraph = Paragraph::new(help_lines)
291            .style(general_style)
292            .block(Block::default())
293            .wrap(ratatui::widgets::Wrap { trim: true });
294
295        let separator = Block::default()
296            .borders(Borders::ALL)
297            .border_type(BorderType::Rounded)
298            .border_style(separator_style);
299
300        let send_reset_link_button_text = if let Some(last_reset_password_link_sent_time) =
301            app.state.last_reset_password_link_sent_time
302        {
303            if last_reset_password_link_sent_time.elapsed()
304                < Duration::from_secs(MIN_TIME_BETWEEN_SENDING_RESET_LINK)
305            {
306                let remaining_time = Duration::from_secs(MIN_TIME_BETWEEN_SENDING_RESET_LINK)
307                    .checked_sub(last_reset_password_link_sent_time.elapsed())
308                    .unwrap();
309                format!("Please wait for {} seconds", remaining_time.as_secs())
310            } else {
311                "Send Reset Link".to_string()
312            }
313        } else {
314            "Send Reset Link".to_string()
315        };
316
317        let email_id_block = Block::default()
318            .style(email_id_field_style)
319            .borders(Borders::ALL)
320            .border_type(BorderType::Rounded);
321
322        let send_reset_link_button = Paragraph::new(send_reset_link_button_text)
323            .style(send_reset_link_button_style)
324            .block(
325                Block::default()
326                    .borders(Borders::ALL)
327                    .border_type(BorderType::Rounded),
328            )
329            .alignment(Alignment::Center);
330
331        let reset_link_block = Block::default()
332            .style(reset_link_field_style)
333            .borders(Borders::ALL)
334            .border_type(BorderType::Rounded);
335
336        let new_password_block = Block::default()
337            .style(new_password_field_style)
338            .borders(Borders::ALL)
339            .border_type(BorderType::Rounded);
340
341        let confirm_new_password_block = Block::default()
342            .style(confirm_new_password_field_style)
343            .borders(Borders::ALL)
344            .border_type(BorderType::Rounded);
345
346        app.state
347            .text_buffers
348            .email_id
349            .set_placeholder_text("Email ID");
350
351        app.state.text_buffers.email_id.set_block(email_id_block);
352
353        app.state
354            .text_buffers
355            .reset_password_link
356            .set_placeholder_text("Reset Link");
357
358        app.state
359            .text_buffers
360            .reset_password_link
361            .set_block(reset_link_block);
362
363        app.state
364            .text_buffers
365            .password
366            .set_placeholder_text("New Password");
367
368        app.state
369            .text_buffers
370            .password
371            .set_block(new_password_block);
372
373        app.state
374            .text_buffers
375            .confirm_password
376            .set_placeholder_text("Confirm New Password");
377
378        app.state
379            .text_buffers
380            .confirm_password
381            .set_block(confirm_new_password_block);
382
383        let show_password_paragraph = Paragraph::new("Show Password")
384            .style(show_password_style)
385            .block(Block::default())
386            .alignment(Alignment::Right);
387
388        let show_password_checkbox_value = if app.state.show_password {
389            "[X]"
390        } else {
391            "[ ]"
392        };
393
394        let show_password_checkbox_paragraph = Paragraph::new(show_password_checkbox_value)
395            .style(show_password_style)
396            .block(Block::default())
397            .alignment(Alignment::Center);
398
399        let submit_button = Paragraph::new("Submit")
400            .style(submit_button_style)
401            .block(
402                Block::default()
403                    // TODO: Think about using the bordered shorthand everywhere
404                    .borders(Borders::ALL)
405                    .border_type(BorderType::Rounded),
406            )
407            .alignment(Alignment::Center);
408
409        rect.render_widget(draw_title(app, main_chunks[0], is_active), main_chunks[0]);
410        rect.render_widget(crab_paragraph, chunks[0]);
411        rect.render_widget(Clear, info_box);
412        render_blank_styled_canvas_with_margin(rect, app, info_box, is_active, -1);
413        rect.render_widget(info_border, info_box);
414        rect.render_widget(info_header, info_chunks[0]);
415        rect.render_widget(help_paragraph, info_chunks[2]);
416        rect.render_widget(separator, chunks[1]);
417        rect.render_widget(app.state.text_buffers.email_id.widget(), email_id_chunk);
418        rect.render_widget(send_reset_link_button, send_reset_link_button_chunk);
419        rect.render_widget(
420            app.state.text_buffers.reset_password_link.widget(),
421            reset_link_chunk,
422        );
423        if app.state.show_password {
424            rect.render_widget(app.state.text_buffers.password.widget(), new_password_chunk);
425            rect.render_widget(
426                app.state.text_buffers.confirm_password.widget(),
427                confirm_new_password_chunk,
428            );
429        } else {
430            if app.state.text_buffers.password.is_empty() {
431                rect.render_widget(app.state.text_buffers.password.widget(), new_password_chunk);
432            } else {
433                let hidden_text = HIDDEN_PASSWORD_SYMBOL
434                    .to_string()
435                    .repeat(app.state.text_buffers.password.get_joined_lines().len());
436                let hidden_paragraph = Paragraph::new(hidden_text)
437                    .style(new_password_field_style)
438                    .block(
439                        Block::default()
440                            .borders(Borders::ALL)
441                            .border_type(BorderType::Rounded),
442                    );
443                rect.render_widget(hidden_paragraph, new_password_chunk);
444            }
445            if app.state.text_buffers.confirm_password.is_empty() {
446                rect.render_widget(
447                    app.state.text_buffers.confirm_password.widget(),
448                    confirm_new_password_chunk,
449                );
450            } else {
451                let hidden_text = HIDDEN_PASSWORD_SYMBOL.to_string().repeat(
452                    app.state
453                        .text_buffers
454                        .confirm_password
455                        .get_joined_lines()
456                        .len(),
457                );
458                let hidden_paragraph = Paragraph::new(hidden_text)
459                    .style(confirm_new_password_field_style)
460                    .block(
461                        Block::default()
462                            .borders(Borders::ALL)
463                            .border_type(BorderType::Rounded),
464                    );
465                rect.render_widget(hidden_paragraph, confirm_new_password_chunk);
466            }
467        }
468        rect.render_widget(show_password_paragraph, show_password_chunks[0]);
469        rect.render_widget(show_password_checkbox_paragraph, show_password_chunks[1]);
470        rect.render_widget(submit_button, submit_button_chunks[1]);
471        if app.config.enable_mouse_support {
472            render_close_button(rect, app, is_active)
473        }
474
475        if app.state.app_status == AppStatus::UserInput {
476            match app.state.focus {
477                Focus::EmailIDField => {
478                    let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
479                        &app.state.text_buffers.email_id,
480                        &app.config.show_line_numbers,
481                        &email_id_chunk,
482                    );
483                    rect.set_cursor_position((x_pos, y_pos));
484                }
485                Focus::ResetPasswordLinkField => {
486                    let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
487                        &app.state.text_buffers.reset_password_link,
488                        &app.config.show_line_numbers,
489                        &reset_link_chunk,
490                    );
491                    rect.set_cursor_position((x_pos, y_pos));
492                }
493                Focus::PasswordField => {
494                    let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
495                        &app.state.text_buffers.password,
496                        &app.config.show_line_numbers,
497                        &new_password_chunk,
498                    );
499                    rect.set_cursor_position((x_pos, y_pos));
500                }
501                Focus::ConfirmPasswordField => {
502                    let (x_pos, y_pos) = calculate_viewport_corrected_cursor_position(
503                        &app.state.text_buffers.confirm_password,
504                        &app.config.show_line_numbers,
505                        &confirm_new_password_chunk,
506                    );
507                    rect.set_cursor_position((x_pos, y_pos));
508                }
509                _ => {}
510            }
511        }
512    }
513}