diff --git a/src/components/lists/single/radio.rs b/src/components/lists/single/radio.rs index 86b4ddf..1cdbc25 100644 --- a/src/components/lists/single/radio.rs +++ b/src/components/lists/single/radio.rs @@ -2,6 +2,7 @@ use crate::common::ValueType; use crate::components::utils::create_unique_id; use leptos::prelude::*; use std::collections::HashMap; +use leptos::html::Input; use crate::components::{use_validation_context, InputState, ValidationState}; #[component] @@ -20,9 +21,7 @@ pub fn Radio( // Using the "peer" class links this input with the div "peer" at the same level. // By using the "peer-checked" selector this div can toggle states based on this // input's state (checked/unchecked) - class=" - relative peer shrink-0 - hidden" + class="relative peer shrink-0 hidden" type="radio" id=id.clone() name=name @@ -43,7 +42,95 @@ pub fn Radio( rotate-45 transform border-3 border-3 rounded-full bg-white border-white transition-opacity duration-150"> - {value.clone()} + {value.clone()} + + } +} + +/// # Ratio Other Component +/// This is a simple "other" field that contains a text input component. It can be selected using the +/// same logic as the [`Radio`] component, and also has a secondary `RwSignal` that contains the +/// current value of the embedded text input. +/// +/// Example usage: +#[component] +fn RadioOther( + #[prop(into)] name: Signal, + #[prop(into)] checked: Signal, + #[prop(into)] on_change: Callback<()>, + #[prop(into)] disabled: Signal, + #[prop(optional, into)] value: RwSignal, + #[prop(into)] dirty: RwSignal, + #[prop(into)] placeholder: Signal, +) -> impl IntoView { + let id = create_unique_id(&name.get_untracked(), &"other".to_string()); + let input_ref = NodeRef::::new(); + + let input_disabled = Signal::derive(move || { + !checked.get() || disabled.get() + }); + + Effect::new(move || { + checked.get() + .then(|| { + if let Some(node) = input_ref.get() { + node.focus().expect("Couldn't focus input element"); + } + }); + }); + + let on_change_text = move |ev| { + let val = event_target_value(&ev); + + value.set(val); + + on_change.run(()); + }; + + let on_blur_text = move |_| { + dirty.set(true); + }; + + view! { + } } @@ -64,24 +151,47 @@ pub fn RadioList( #[prop(optional, into)] disabled: Signal, #[prop(optional, into)] label: String, #[prop(optional, into)] required: Signal, + #[prop(optional, into)] other_prompt: Signal, ) -> impl IntoView { //#region Value Logic // Store a single signal that is the currently selected value. We can only have one selected // value, so there's no list needed. The default selected value is whatever the map has selected // already, or nothing. - let default_value = match data_map.get_untracked().get(&data_member.get_untracked()) { - Some(ValueType::String(Some(value))) => value.clone(), - _ => String::new() - }; - let selected_value = RwSignal::new(default_value); + let hydrated = RwSignal::new(false); + let selected_value = RwSignal::new(String::new()); + let other_checked = RwSignal::new(false); + let other_value = RwSignal::new(String::new()); - // When the selected value changes, update the data map at the correct location + // Hydration effect that resolves data. Sets up all data properly. + Effect::new({ + let items = items.clone(); + move || { + if hydrated.get() { + return; + } + + let default_value = match data_map.get_untracked().get(&data_member.get_untracked()) { + Some(ValueType::String(Some(value))) => value.clone(), + _ => String::new() + }; + + if !items.contains(&default_value) && !default_value.is_empty() { + other_checked.set(true); + other_value.set(default_value.clone()); + } + + selected_value.set(default_value); + + hydrated.set(true); + } + }); + + // When the other option is selected and the other value changes, update the selected value. Effect::new(move || { - data_map.update(|map| { - map.insert(data_member.get(), ValueType::String(Some(selected_value.get()))); - }); + other_checked.get() + .then(|| selected_value.set(other_value.get())); }); //#endregion @@ -109,7 +219,15 @@ pub fn RadioList( list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name) }); }); - + + // When the selected value changes, update the data map at the correct location and validate. + Effect::new(move || { + error.set(validate(selected_value.get(), required.get())); + data_map.update(|map| { + map.insert(data_member.get(), ValueType::String(Some(selected_value.get()))); + }); + }); + //#endregion //#region Render Logic @@ -123,12 +241,13 @@ pub fn RadioList( let item = RwSignal::new(item); let checked_signal = Signal::derive(move || { - selected_value.get() == item.get() + selected_value.get() == item.get() && !other_checked.get() }); let on_change = move || { // on_change only runs when the radio button is checked, not when unchecked. // Change the selected value to the new one. + other_checked.set(false); selected_value.set(item.get()); error.set(validate(selected_value.get_untracked(), required.get_untracked())); dirty.set(true); @@ -146,6 +265,18 @@ pub fn RadioList( } }) .collect::>()} + + + +
diff --git a/src/pages/api/comparisons.rs b/src/pages/api/comparisons.rs index b60acef..daf1339 100644 --- a/src/pages/api/comparisons.rs +++ b/src/pages/api/comparisons.rs @@ -15,6 +15,7 @@ use crate::common::{ use leptos::prelude::ServerFnError; use leptos::server; +#[cfg(feature = "ssr")] static COMPARISONS_TABLE: &str = "leptos-comparison-test"; /// Helper function to create all sports comparisons. Only used to create initial diff --git a/src/pages/api/scholarships.rs b/src/pages/api/scholarships.rs index aaf5d47..d467a2e 100644 --- a/src/pages/api/scholarships.rs +++ b/src/pages/api/scholarships.rs @@ -17,6 +17,7 @@ use crate::common::ExpandableInfo; use leptos::prelude::ServerFnError; use leptos::server; +#[cfg(feature = "ssr")] static SCHOLARSHIPS_TABLE: &str = "leptos-scholarship-test"; #[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")] diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 6c47dcc..3481a5b 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -8,7 +8,7 @@ use std::process::{Command, Stdio}; #[cfg(feature = "ssr")] use std::io::Write; -use crate::common::{ComparisonData, ExpandableInfo}; +use crate::common::ExpandableInfo; use crate::pages::UnauthenticatedPage; use crate::pages::utils::get_user_claims; use crate::components::{ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm}; @@ -22,7 +22,6 @@ use leptos::task::spawn_local; use leptos::web_sys::HtmlAnchorElement; use leptos::wasm_bindgen::JsCast; use std::collections::HashMap; -use crate::pages::api::get_comparisons_categorized; /// # Get Student Info /// Gets a student's information given their `subject`. @@ -210,28 +209,6 @@ pub fn HomePage() -> impl IntoView { }; //#endregion - let comparison_resource = Resource::new( - move || user_subject.get(), - async move |_| get_comparisons_categorized().await - ); - - let comparison_info = RwSignal::new(HashMap::>::new()); - - Effect::new(move || { - match comparison_resource.get() { - Some(Ok(comparisons)) => { - log!("Found comparison info: {:?}", comparisons); - comparison_info.set(comparisons); - } - Some(Err(err)) => { - log!("Error while getting comparisons: {:?}", err); - } - _ => { - log!("An unknown error occurred."); - } - } - }); - view! { @@ -333,6 +310,7 @@ pub fn HomePage() -> impl IntoView { data_member="town" data_map=form_data required=true + other_prompt="Other..." /> diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index df41afd..69130ac 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -361,42 +361,7 @@ fn ScholarshipForm( /// submission fails, this function is not run. #[prop(into)] on_submit: Callback<()> ) -> impl IntoView { - let submit_action = ServerAction::::new(); - - let submit_status = RwSignal::new(SubmitStatus::Idle); - let elements_disabled = Signal::derive(move || { - matches!(submit_action.pending().get(), true) - }); - - Effect::new(move || { - if submit_action.pending().get() { - submit_status.set(SubmitStatus::Sending); - return; - } - - if let Some(result) = submit_action.value().get() { - match result { - Ok(()) => { - submit_status.set(SubmitStatus::Success); - on_submit.run(()); - }, - Err(_err) => submit_status.set(SubmitStatus::Error(())) - } - } - }); - - let comparison_lists = Resource::new( - move || scholarship_id.get().is_some(), - async move |_| { - get_comparisons_categorized().await - } - ); - - Effect::new(move || { - if let Some(Ok(map)) = comparison_lists.get() { - log!("Available categories: {:?}", map.keys()); - } - }); + //#region Form Data Setup let scholarship_info = Resource::new( move || scholarship_id.get(), @@ -428,6 +393,22 @@ fn ScholarshipForm( } }); + //#endregion + //#region Comparison Logic + + let comparison_lists = Resource::new( + move || scholarship_id.get().is_some(), + async move |_| { + get_comparisons_categorized().await + } + ); + + Effect::new(move || { + if let Some(Ok(map)) = comparison_lists.get() { + log!("Available categories: {:?}", map.keys()); + } + }); + let get_comp_category = move |category: &str| { if let Some(Ok(map)) = comparison_lists.get() { map.get(category).unwrap_or(&Vec::new()).clone() @@ -469,6 +450,33 @@ fn ScholarshipForm( let major_ids = Signal::derive(move || get_comp_ids("Majors")); let major_text = Signal::derive(move || get_comp_text("Majors")); + //#endregion + //#region Submission Logic + + let submit_action = ServerAction::::new(); + + let submit_status = RwSignal::new(SubmitStatus::Idle); + let elements_disabled = Signal::derive(move || { + matches!(submit_action.pending().get(), true) + }); + + Effect::new(move || { + if submit_action.pending().get() { + submit_status.set(SubmitStatus::Sending); + return; + } + + if let Some(result) = submit_action.value().get() { + match result { + Ok(()) => { + submit_status.set(SubmitStatus::Success); + on_submit.run(()); + }, + Err(_err) => submit_status.set(SubmitStatus::Error(())) + } + } + }); + let mut toasts = expect_context::(); let on_submit = move |_| { @@ -481,7 +489,7 @@ fn ScholarshipForm( info }); }; - + Effect::new(move || { submit_action.value().get().is_some() .then(|| { @@ -496,6 +504,8 @@ fn ScholarshipForm( }); }); + //#endregion + let create_comps = ServerAction::::new(); view! { @@ -586,25 +596,21 @@ fn ScholarshipForm( label="Will the award be made to the school or the student?" /> - - -