diff --git a/Cargo.lock b/Cargo.lock index bdc13b6..9308cba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,7 @@ dependencies = [ "getrandom 0.2.15", "gloo-storage 0.3.0", "http-api-isahc-client", + "input-rs", "jsonwebtoken", "mongodb", "rand 0.8.5", @@ -2390,6 +2391,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" +[[package]] +name = "input-rs" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab51dec768b54f00586c58d7e4df9d66a0a54b384e2b2ce8ad7ae9b7bd1ff131" +dependencies = [ + "dioxus", + "web-sys", +] + [[package]] name = "instant" version = "0.1.13" diff --git a/Cargo.toml b/Cargo.toml index 1460619..959b576 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ anyhow = "1.0.93" time = "0.3.36" regex = "1.11.1" gloo-storage = "0.3.0" +input-rs = { version = "0.2.2", features = ["dio"] } # Debug dioxus-logger = "0.5.1" diff --git a/src/components/dashboard/books/create.rs b/src/components/dashboard/books/create.rs index de6d94c..e372646 100644 --- a/src/components/dashboard/books/create.rs +++ b/src/components/dashboard/books/create.rs @@ -1,7 +1,5 @@ use crate::components::dashboard::books::list::CachedBooksData; use crate::components::dashboard::books::list::CACHE_KEY; -use crate::components::dashboard::fields::input::InputField; -use crate::components::dashboard::fields::number::NumberField; use crate::components::dashboard::fields::select::SelectField; use crate::components::spinner::Spinner; use crate::components::spinner::SpinnerSize; @@ -16,6 +14,11 @@ use chrono::Duration; use chrono::Utc; use dioxus::prelude::*; use gloo_storage::{LocalStorage, Storage}; +use input_rs::dioxus::Input; + +pub fn validate_input(field: String) -> bool { + !&field.is_empty() +} #[component] pub fn CreateBookPanel(user_token: Signal) -> Element { @@ -24,21 +27,20 @@ pub fn CreateBookPanel(user_token: Signal) -> Element { let title = use_signal(|| "".to_string()); let subtitle = use_signal(|| "".to_string()); let model = use_signal(|| "gemini-1.5-flash".to_string()); - let subtopics = use_signal(|| 3); - let chapters = use_signal(|| 5); + let subtopics = use_signal(|| "3".to_string()); + let chapters = use_signal(|| "5".to_string()); let language = use_signal(|| "English".to_string()); - let max_length = use_signal(|| 1000); + let max_length = use_signal(|| "1000".to_string()); let title_valid = use_signal(|| true); let subtitle_valid = use_signal(|| true); + let subtopics_valid = use_signal(|| true); let language_valid = use_signal(|| true); + let maxlen_valid = use_signal(|| true); + let chapters_valid = use_signal(|| true); let mut loading = use_signal(|| false); let _form_error = use_signal(|| None::); - let validate_title = |title: &str| !title.is_empty(); - let validate_subtitle = |subtitle: &str| !subtitle.is_empty(); - let validate_language = |language: &str| !language.is_empty(); - let mut toasts_manager = use_context::>(); let handle_submit = move |e: Event| { @@ -47,7 +49,7 @@ pub fn CreateBookPanel(user_token: Signal) -> Element { let subtitle_value = subtitle().clone(); loading.set(true); - if !validate_title(&title_value) || !validate_subtitle(&subtitle_value) { + if !validate_input(title_value) || !validate_input(subtitle_value) { // form_error.set(Some("Title and subtitle are required.".to_string())); toasts_manager.set( toasts_manager() @@ -185,13 +187,121 @@ pub fn CreateBookPanel(user_token: Signal) -> Element { h2 { class: "text-xl font-semibold mb-4", "Generate" } form { class: "space-y-4", onsubmit: handle_submit, - InputField { label: "Title", value: title, is_valid: title_valid, validate: validate_title, required: true } - InputField { label: "Subtitle", value: subtitle, is_valid: subtitle_valid, validate: validate_subtitle, required: true } + Input { + r#type: "text", + label: "Title", + handle: title, + placeholder: "Title", + error_message: "Title can't be blank!", + required: true, + valid_handle: title_valid, + validate_function: validate_input, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && title_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } + Input { + r#type: "text", + label: "Subtitle", + handle: subtitle, + placeholder: "Subtitle", + error_message: "Subtitle can't be blank!", + required: true, + valid_handle: subtitle_valid, + validate_function: validate_input, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && subtitle_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } SelectField { label: "Model", options: vec!["gemini-pro", "gemini-1.0-pro", "gemini-1.5-pro", "gemini-1.5-flash"], selected: model } - NumberField { label: "Subtopics per Chapter", value: subtopics, required: true } - NumberField { label: "Chapters", value: chapters, required: true } - InputField { label: "Language", value: language, is_valid: language_valid, validate: validate_language, required: true } - NumberField { label: "Max Length", value: max_length, required: true } + Input { + r#type: "number", + label: "Subtopics per Chapter", + handle: subtopics, + placeholder: "Subtopics", + error_message: "Subtopics can't be blank!", + required: true, + valid_handle: subtopics_valid, + validate_function: validate_input, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && subtopics_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } + Input { + r#type: "number", + label: "Chapters", + handle: chapters, + placeholder: "Chapters", + error_message: "Chapters can't be blank!", + required: true, + valid_handle: chapters_valid, + validate_function: validate_input, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && chapters_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } + Input { + r#type: "text", + label: "Language", + handle: language, + placeholder: "Language", + error_message: "Language can't be blank!", + required: true, + valid_handle: language_valid, + validate_function: validate_input, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && language_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } + Input { + r#type: "number", + label: "Max Length", + handle: max_length, + placeholder: "Max Length", + error_message: "Language can't be blank!", + required: true, + valid_handle: maxlen_valid, + validate_function: validate_input, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && maxlen_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } // if let Some(error) = &form_error() { // p { class: "text-red-600", "{error}" } // } diff --git a/src/components/dashboard/fields.rs b/src/components/dashboard/fields.rs index e78cb24..8f398ba 100644 --- a/src/components/dashboard/fields.rs +++ b/src/components/dashboard/fields.rs @@ -1,3 +1 @@ -pub(crate) mod input; -pub(crate) mod number; pub(crate) mod select; diff --git a/src/components/dashboard/fields/input.rs b/src/components/dashboard/fields/input.rs deleted file mode 100644 index 81c43a8..0000000 --- a/src/components/dashboard/fields/input.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::theme::Theme; -use dioxus::prelude::*; - -#[component] -pub fn InputField( - label: &'static str, - value: Signal, - is_valid: Signal, - validate: fn(&str) -> bool, - required: bool, -) -> Element { - let theme = use_context::>(); - let dark_mode = theme() == Theme::Dark; - - let handle_input = move |e: Event| { - let input_value = e.value().clone(); - value.set(input_value.clone()); - is_valid.set(validate(&input_value)); - }; - - rsx! { - div { - label { - class: format!("block text-sm font-medium {}", if dark_mode { "text-gray-300" } else { "text-gray-700" }), - "{label}" - } - input { - class: format!( - "mt-1 block w-full p-2 border rounded-md shadow-sm {} {}", - if dark_mode { "bg-gray-900" } else { "" }, - if is_valid() { "border-gray-300" } else { "border-red-500" - }), - value: "{value}", - oninput: handle_input, - required: required - } - if !is_valid() { - p { class: "text-red-500 text-sm mt-1", "Invalid input" } - } - } - } -} diff --git a/src/components/dashboard/fields/number.rs b/src/components/dashboard/fields/number.rs deleted file mode 100644 index e151ee7..0000000 --- a/src/components/dashboard/fields/number.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::theme::Theme; -use dioxus::prelude::*; - -#[component] -pub fn NumberField(label: &'static str, value: Signal, required: bool) -> Element { - let theme = use_context::>(); - let dark_mode = theme() == Theme::Dark; - rsx! { - div { - label { class: format!("block text-sm font-medium {}", if dark_mode { "text-gray-300" } else { "text-gray-700" }), "{label}" } - input { - r#type: "number", - class: format!("mt-1 block w-full p-2 border rounded-md shadow-sm {}", if dark_mode { "bg-gray-900 border-gray-700" } else { "border-gray-300" }), - value: "{value}", - oninput: move |e| if let Ok(val) = e.value().parse() { value.set(val); }, - required: required - } - } - } -} diff --git a/src/components/dashboard/profile/edit.rs b/src/components/dashboard/profile/edit.rs index f348842..dcc8739 100644 --- a/src/components/dashboard/profile/edit.rs +++ b/src/components/dashboard/profile/edit.rs @@ -1,10 +1,34 @@ -use crate::components::dashboard::fields::input::InputField; use crate::components::dashboard::profile::view::ProfileDetailsProps; use crate::components::toast::manager::{ToastManager, ToastType}; use crate::server::auth::controller::edit_profile; use crate::server::auth::request::EditUserSchema; use chrono::Duration; use dioxus::prelude::*; +use input_rs::dioxus::Input; + +fn validate_name(name: String) -> bool { + !name.is_empty() +} + +fn validate_email(email: String) -> bool { + email.contains('@') && email.contains('.') +} + +fn validate_photo(photo: String) -> bool { + !photo.is_empty() +} + +fn validate_old_password(password: String) -> bool { + !password.is_empty() +} + +fn validate_new_password(password: String) -> bool { + password.len() >= 8 +} + +fn validate_confirm_password(confirm: String, new: String) -> bool { + confirm == new +} #[component] pub fn ProfileForm(props: ProfileDetailsProps) -> Element { @@ -22,19 +46,13 @@ pub fn ProfileForm(props: ProfileDetailsProps) -> Element { let confirm_password = use_signal(|| String::new()); let mut name_valid = use_signal(|| true); - let validate_name = |name: &str| !name.is_empty(); let mut email_valid = use_signal(|| true); - let validate_email = |email: &str| email.contains("@") && email.contains("."); let photo_valid = use_signal(|| true); - let validate_photo = |photo: &str| !photo.is_empty(); let mut old_password_valid = use_signal(|| true); - let validate_old_password = |password: &str| !password.is_empty(); let mut new_password_valid = use_signal(|| true); - let validate_new_password = |password: &str| password.len() >= 8; let mut confirm_password_valid = use_signal(|| true); - let validate_confirm_password = |confirm: &str, new: &str| confirm == new; let navigator = use_navigator(); let mut toasts_manager = use_context::>(); @@ -45,35 +63,35 @@ pub fn ProfileForm(props: ProfileDetailsProps) -> Element { let mut all_valid = true; - if !validate_name(&name()) { + if !validate_name(name()) { name_valid.set(false); all_valid = false; } else { name_valid.set(true); } - if !validate_email(&email()) { + if !validate_email(email()) { email_valid.set(false); all_valid = false; } else { email_valid.set(true); } - if !validate_old_password(&old_password()) { + if !validate_old_password(old_password()) { old_password_valid.set(false); all_valid = false; } else { old_password_valid.set(true); } - if !validate_new_password(&new_password()) { + if !validate_new_password(new_password()) { new_password_valid.set(false); all_valid = false; } else { new_password_valid.set(true); } - if !validate_confirm_password(&confirm_password(), &new_password()) { + if !validate_confirm_password(confirm_password(), new_password()) { confirm_password_valid.set(false); all_valid = false; } else { @@ -145,48 +163,124 @@ pub fn ProfileForm(props: ProfileDetailsProps) -> Element { rsx!( form { class: "space-y-4", onsubmit: handle_submit, - InputField { + Input { + r#type: "text", label: "Name", - value: name, - is_valid: name_valid, - validate: validate_name, - required: true - }, - InputField { + handle: name, + placeholder: "Name", + error_message: "Name can't be blank!", + required: true, + valid_handle: name_valid, + validate_function: validate_name, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && name_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } + , + Input { + r#type: "text", label: "Image", - value: photo, - is_valid: photo_valid, - validate: validate_photo, - required: true - }, - InputField { + handle: photo, + placeholder: "Image URL", + error_message: "Image can't be blank!", + required: true, + valid_handle: photo_valid, + validate_function: validate_photo, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && photo_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } + , + Input { + r#type: "email", label: "Email", - value: email, - is_valid: email_valid, - validate: validate_email, - required: true - }, - InputField { + handle: email, + placeholder: "Email", + error_message: "Enter a valid email address!", + required: true, + valid_handle: email_valid, + validate_function: validate_email, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && email_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } + , + Input { + r#type: "password", label: "Old Password", - value: old_password, - is_valid: old_password_valid, - validate: validate_old_password, - required: true - }, - InputField { + handle: old_password, + placeholder: "Old Password", + error_message: "Old password can't be blank!", + required: true, + valid_handle: old_password_valid, + validate_function: validate_old_password, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && old_password_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } + , + Input { + r#type: "password", label: "New Password", - value: new_password, - is_valid: new_password_valid, - validate: validate_new_password, - required: true + handle: new_password, + placeholder: "New Password", + error_message: "Password must be at least 8 characters!", + required: true, + valid_handle: new_password_valid, + validate_function: validate_new_password, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && new_password_valid() { + "border-gray-300 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", }, - InputField { + Input { + r#type: "password", label: "Confirm Password", - value: confirm_password, - is_valid: confirm_password_valid, - validate: validate_new_password, - required: true - }, + handle: confirm_password, + placeholder: "Confirm Password", + error_message: "Passwords do not match!", + required: true, + valid_handle: confirm_password_valid, + validate_function: validate_new_password, + class: "field mb-6", + field_class: "validate-input mb-6", + label_class: if dark_mode { "block text-sm font-medium text-gray-300" } else { "block text-sm font-medium text-gray-700" }, + input_class: if dark_mode && confirm_password_valid() { + "border-gray-300 bg-gray-900 mt-1 block w/full p-2 border rounded-md shadow-sm" + } else { + "border-red-500 bg-gray-900 mt-1 block w-full p-2 border rounded-md shadow-sm" + }, + error_class: "text-red-500 text-sm mt-1", + } button { class: format!("py-2 px-4 rounded-md {}", if dark_mode { "bg-blue-600" } else { "bg-blue-500 text-white" }), r#type: "submit", diff --git a/src/server/book/request.rs b/src/server/book/request.rs index 2df6124..cbfcb17 100644 --- a/src/server/book/request.rs +++ b/src/server/book/request.rs @@ -34,10 +34,10 @@ pub struct GenerateBookRequest { pub subtitle: String, pub token: String, pub model: String, - pub subtopics: u64, - pub chapters: u64, + pub subtopics: String, + pub chapters: String, pub language: String, - pub max_length: u64, + pub max_length: String, } #[derive(Debug, Serialize, Deserialize, Clone)]