diff --git a/client/src/modes/general/dialog.rs b/client/src/modes/general/dialog.rs index f785cb5..c4b992f 100644 --- a/client/src/modes/general/dialog.rs +++ b/client/src/modes/general/dialog.rs @@ -2,12 +2,13 @@ use crossterm::event::{Event, KeyCode}; use crate::config::binds::config::Config; -use crate::state::state::AppState; use crate::ui::handlers::context::DialogPurpose; +use crate::state::state::AppState; use crate::state::pages::auth::AuthState; +use crate::state::pages::auth::RegisterState; use crate::services::auth::AuthClient; use crate::modes::handlers::event::EventOutcome; -use crate::tui::functions::common::login; +use crate::tui::functions::common::{login, register}; /// Handles key events specifically when a dialog is active. /// Returns Some(Result) if the event was handled (consumed), @@ -17,6 +18,7 @@ pub async fn handle_dialog_event( config: &Config, app_state: &mut AppState, auth_state: &mut AuthState, + register_state: &mut RegisterState, auth_client: &mut AuthClient, ) -> Option>> { if let Event::Key(key) = event { @@ -85,7 +87,32 @@ pub async fn handle_dialog_event( } } } - // Add cases for other DialogPurpose variants here if needed + DialogPurpose::RegisterSuccess => { // Add this arm + match selected_index { + 0 => { // "OK" button for RegisterSuccess + app_state.hide_dialog(); + // Go back to intro after successful registration dialog + let message = register::back_to_main(register_state, app_state).await; + return Some(Ok(EventOutcome::Ok(message))); + } + _ => { // Default for RegisterSuccess + app_state.hide_dialog(); + return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string()))); + } + } + } + DialogPurpose::RegisterFailed => { // Add this arm + match selected_index { + 0 => { // "OK" button for RegisterFailed + app_state.hide_dialog(); // Just dismiss + return Some(Ok(EventOutcome::Ok("Register failed dialog dismissed".to_string()))); + } + _ => { // Default for RegisterFailed + app_state.hide_dialog(); + return Some(Ok(EventOutcome::Ok("Unknown dialog button selected".to_string()))); + } + } + } } } _ => {} // Ignore other general actions when dialog is shown diff --git a/client/src/modes/general/navigation.rs b/client/src/modes/general/navigation.rs index 51dc1b7..3692dd7 100644 --- a/client/src/modes/general/navigation.rs +++ b/client/src/modes/general/navigation.rs @@ -60,6 +60,8 @@ pub async fn handle_navigation_event( (UiContext::Intro, app_state.ui.intro_state.selected_option) } else if app_state.ui.show_login && app_state.ui.focus_outside_canvas { (UiContext::Login, app_state.general.selected_item) + } else if app_state.ui.show_register && app_state.ui.focus_outside_canvas { + (UiContext::Register, app_state.general.selected_item) } else if app_state.ui.show_admin { (UiContext::Admin, app_state.general.selected_item) } else if app_state.ui.dialog.dialog_show { diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index fb23108..d09ca40 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -13,7 +13,7 @@ use crate::state::canvas_state::CanvasState; use crate::ui::handlers::rat_state::UiStateHandler; use crate::ui::handlers::context::UiContext; use crate::tui::functions::{intro, admin}; -use crate::tui::functions::common::login; +use crate::tui::functions::common::{login, register}; use crate::modes::{ common::command_mode, canvas::{edit, read_only, common_mode}, @@ -76,7 +76,7 @@ impl EventHandler { // --- DIALOG MODALITY --- if app_state.ui.dialog.dialog_show { if let Some(dialog_result) = dialog::handle_dialog_event( - &event, config, app_state, auth_state, &mut self.auth_client + &event, config, app_state, auth_state, register_state, &mut self.auth_client ).await { return dialog_result; } @@ -122,6 +122,13 @@ impl EventHandler { _ => "Invalid Login Option".to_string(), }; } + UiContext::Register => { + message = match index { + 0 => register::save(register_state, &mut self.auth_client, app_state).await?, + 1 => register::back_to_main(register_state, app_state).await, + _ => "Invalid Login Option".to_string(), + }; + } UiContext::Admin => { // Assuming handle_admin_selection uses app_state.general.selected_item admin::handle_admin_selection(app_state); diff --git a/client/src/tui/functions/common.rs b/client/src/tui/functions/common.rs index 33cb712..54b3140 100644 --- a/client/src/tui/functions/common.rs +++ b/client/src/tui/functions/common.rs @@ -2,4 +2,4 @@ pub mod form; pub mod login; - +pub mod register; diff --git a/client/src/tui/functions/common/register.rs b/client/src/tui/functions/common/register.rs new file mode 100644 index 0000000..0700516 --- /dev/null +++ b/client/src/tui/functions/common/register.rs @@ -0,0 +1,148 @@ +// src/tui/functions/common/register.rs + +use crate::{ + services::auth::AuthClient, + state::{ + pages::auth::RegisterState, + state::AppState, + canvas_state::CanvasState, + }, + ui::handlers::context::DialogPurpose, +}; + +/// Attempts to register the user using the provided details via gRPC. +/// Updates RegisterState and AppState on success or failure. +pub async fn save( + register_state: &mut RegisterState, + auth_client: &mut AuthClient, + app_state: &mut AppState, +) -> Result> { + let username = register_state.username.clone(); + let email = register_state.email.clone(); + // Handle optional passwords: send None if empty, Some(value) otherwise + let password = if register_state.password.is_empty() { + None + } else { + Some(register_state.password.clone()) + }; + let password_confirmation = if register_state.password_confirmation.is_empty() { + None + } else { + Some(register_state.password_confirmation.clone()) + }; + + // Basic client-side validation (example) + if username.is_empty() { + app_state.show_dialog( + "Registration Failed", + "Username cannot be empty.", + vec!["OK".to_string()], + DialogPurpose::RegisterFailed, + ); + register_state.error_message = Some("Username cannot be empty.".to_string()); + return Ok("Registration failed: Username cannot be empty.".to_string()); + } + if password.is_some() && password != password_confirmation { + app_state.show_dialog( + "Registration Failed", + "Passwords do not match.", + vec!["OK".to_string()], + DialogPurpose::RegisterFailed, + ); + register_state.error_message = Some("Passwords do not match.".to_string()); + return Ok("Registration failed: Passwords do not match.".to_string()); + } + + + // Clear previous error/dialog state before attempting + register_state.error_message = None; + app_state.hide_dialog(); + + // Call the gRPC register method + match auth_client.register(username, email, password, password_confirmation).await { + Ok(response) => { + // Clear fields on success? Optional, maybe wait for dialog confirmation. + // register_state.username.clear(); + // register_state.email.clear(); + // register_state.password.clear(); + // register_state.password_confirmation.clear(); + register_state.set_has_unsaved_changes(false); + + let success_message = format!( + "Registration Successful!\n\n\ + User ID: {}\n\ + Username: {}\n\ + Email: {}\n\ + Role: {}", + response.id, + response.username, + response.email, + response.role + ); + + // Show success dialog + app_state.show_dialog( + "Registration Success", + &success_message, + vec!["OK".to_string()], // Simple OK for now + DialogPurpose::RegisterSuccess, + ); + + Ok("Registration successful, details shown in dialog.".to_string()) + } + Err(e) => { + let error_message = format!("{}", e); + register_state.error_message = Some(error_message.clone()); + register_state.set_has_unsaved_changes(true); // Keep changes on error + + // Show error dialog + app_state.show_dialog( + "Registration Failed", + &error_message, + vec!["OK".to_string()], + DialogPurpose::RegisterFailed, + ); + + Ok(format!("Registration failed: {}", error_message)) + } + } +} + +/// Clears the registration form fields. +pub async fn revert( + register_state: &mut RegisterState, + _app_state: &mut AppState, // Keep signature consistent if needed elsewhere +) -> String { + register_state.username.clear(); + register_state.email.clear(); + register_state.password.clear(); + register_state.password_confirmation.clear(); + register_state.error_message = None; + register_state.set_has_unsaved_changes(false); + register_state.current_field = 0; // Reset focus to first field + register_state.current_cursor_pos = 0; + "Registration form cleared".to_string() +} + +/// Clears the form and returns to the intro screen. +pub async fn back_to_main( + register_state: &mut RegisterState, + app_state: &mut AppState, +) -> String { + // Clear fields first + let _ = revert(register_state, app_state).await; + + // Ensure dialog is hidden + app_state.hide_dialog(); + + // Navigation logic + app_state.ui.show_register = false; + app_state.ui.show_intro = true; + + // Reset focus state + app_state.ui.focus_outside_canvas = false; + app_state.general.selected_item = 0; // Reset intro selection + + "Returned to main menu".to_string() +} + diff --git a/client/src/ui/handlers/context.rs b/client/src/ui/handlers/context.rs index 7c54eb8..3328d61 100644 --- a/client/src/ui/handlers/context.rs +++ b/client/src/ui/handlers/context.rs @@ -4,6 +4,7 @@ pub enum UiContext { Intro, Login, + Register, Admin, Dialog, } @@ -12,6 +13,8 @@ pub enum UiContext { pub enum DialogPurpose { LoginSuccess, LoginFailed, + RegisterSuccess, + RegisterFailed, // TODO in the future: // ConfirmQuit, }