Add major comparisons #45

Merged
BobdaFett merged 4 commits from feature/categorize-chips into main 2026-01-14 03:23:57 +00:00
9 changed files with 148 additions and 46 deletions

No files matched your search

+4 -4
View File
@@ -41,11 +41,11 @@ pub fn Checkbox(
#[prop(into)] value: Signal<String>, #[prop(into)] value: Signal<String>,
/// The name of the checkbox. This is used to group checkboxes together, so every checkbox /// The name of the checkbox. This is used to group checkboxes together, so every checkbox
/// in a group should have the same name. /// in a group should have the same name.
#[prop(into)] name: String, #[prop(into)] name: Signal<String>,
/// A [`Signal`] indicating whether the checkbox is disabled. /// A [`Signal`] indicating whether the checkbox is disabled.
#[prop(into)] disabled: Signal<bool>, #[prop(into)] disabled: Signal<bool>,
) -> impl IntoView { ) -> impl IntoView {
let id = create_unique_id(&name, &value.get_untracked()); let id = create_unique_id(&name.get(), &value.get_untracked());
view! { view! {
<label for=id class="flex items-center"> <label for=id class="flex items-center">
@@ -104,7 +104,7 @@ pub fn Checkbox(
#[component] #[component]
pub fn CheckboxList( pub fn CheckboxList(
/// The data member that this list should edit/display from the `data_map`. /// The data member that this list should edit/display from the `data_map`.
#[prop(into)] data_member: String, #[prop(into)] data_member: Signal<String>,
/// A [`Signal`] allowing access to a data store. /// A [`Signal`] allowing access to a data store.
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>, #[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
/// The available choices for this list. /// The available choices for this list.
@@ -159,7 +159,7 @@ pub fn CheckboxList(
on_change=on_change on_change=on_change
// The actual selected values are tracked by this element, not by the checkboxes themselves. // The actual selected values are tracked by this element, not by the checkboxes themselves.
value=item value=item
name=data_member.clone() name=data_member
disabled=disabled disabled=disabled
/> />
} }
+4 -4
View File
@@ -27,14 +27,14 @@ use super::utils::use_selectable_list;
pub fn Chip( pub fn Chip(
#[prop(into)] display_text: Signal<String>, #[prop(into)] display_text: Signal<String>,
#[prop(into)] value: Signal<String>, #[prop(into)] value: Signal<String>,
#[prop(into)] name: String, #[prop(into)] name: Signal<String>,
#[prop(into)] on_change: Callback<(), ()>, #[prop(into)] on_change: Callback<(), ()>,
#[prop()] checked: Signal<bool>, #[prop()] checked: Signal<bool>,
#[prop(optional, into)] disabled: Signal<bool>, #[prop(optional, into)] disabled: Signal<bool>,
) -> impl IntoView { ) -> impl IntoView {
// Generate a unique id - ensure that the value doesn't contain spaces. // Generate a unique id - ensure that the value doesn't contain spaces.
// Without this id, checkbox/radio inputs can interfere with each other. // Without this id, checkbox/radio inputs can interfere with each other.
let id = create_unique_id(&name, &value.get_untracked()); let id = create_unique_id(&name.get(), &value.get_untracked());
view! { view! {
<label for=id> <label for=id>
@@ -71,7 +71,7 @@ pub fn Chip(
#[component] #[component]
pub fn ChipsList( pub fn ChipsList(
/// The member in the data map that this input affects. /// The member in the data map that this input affects.
#[prop(into)] data_member: String, #[prop(into)] data_member: Signal<String>,
/// The data map that contains the list of values. This will be edited as the input changes. /// The data map that contains the list of values. This will be edited as the input changes.
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>, #[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
/// The list of strings that should be displayed in the list. /// The list of strings that should be displayed in the list.
@@ -132,7 +132,7 @@ pub fn ChipsList(
on_change=on_change on_change=on_change
value=value value=value
display_text=text display_text=text
name=data_member.clone() name=data_member
disabled=disabled disabled=disabled
/> />
} }
+32 -13
View File
@@ -23,24 +23,37 @@ fn value_type_to_vec(value_type: &ValueType) -> Vec<String> {
} }
pub fn use_selectable_list( pub fn use_selectable_list(
data_member: String, data_member: Signal<String>,
data_map: RwSignal<HashMap<String, ValueType>>, data_map: RwSignal<HashMap<String, ValueType>>,
required: Signal<bool> required: Signal<bool>
) -> ListController { ) -> ListController {
//#region Central List Logic //#region Central List Logic
// Get default values from the data map. This will only run once, when the input is created
// and mounted to the DOM.
let temp_map = data_map.get_untracked();
let origin_value_type = temp_map.get(&data_member)
.unwrap_or(&ValueType::List(None));
let origin_list = value_type_to_vec(origin_value_type);
// "Raw" selected values - just strings, no ValueTyping // "Raw" selected values - just strings, no ValueTyping
let selected_list = RwSignal::new(origin_list); let selected_list = RwSignal::new(Vec::new());
let hydrated = RwSignal::new(false);
let dirty = RwSignal::new(false);
// Hydration effect - runs only once, to hydrate data.
Effect::new(move || {
if hydrated.get() {
return;
}
// React to data_map changes
let map = data_map.get();
let key = data_member.get();
let list = map
.get(&key)
.unwrap_or(&ValueType::List(None));
selected_list.set(value_type_to_vec(list));
hydrated.set(true);
});
Effect::new({ Effect::new({
let data_member = data_member.clone();
move || { move || {
// Convert selected strings into ValueTypes // Convert selected strings into ValueTypes
let typed_list = selected_list.with(|list| let typed_list = selected_list.with(|list|
@@ -51,7 +64,7 @@ pub fn use_selectable_list(
// Update the map // Update the map
data_map.update(|map| { data_map.update(|map| {
map.insert(data_member.clone(), ValueType::List(Some(typed_list))); map.insert(data_member.get(), ValueType::List(Some(typed_list)));
}); });
} }
}); });
@@ -69,17 +82,23 @@ pub fn use_selectable_list(
ValidationState::Valid ValidationState::Valid
} }
}); });
let dirty = RwSignal::new(false);
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_))); let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
let validator = RwSignal::new(InputState::new( let validator = RwSignal::new(InputState::new(
data_member.clone(), data_member.get(),
error.clone(), error.clone(),
dirty.clone() dirty.clone()
)); ));
validation_context.validators.update(|list| list.push(validator.clone())); validation_context.validators.update(|list| list.push(validator.clone()));
on_cleanup(move || {
validation_context.validators.update(|list| {
list.retain(|v| *v.get_untracked().input_name != *validator.get_untracked().input_name)
});
});
//#endregion //#endregion
ListController { ListController {
+9 -3
View File
@@ -96,14 +96,20 @@ pub fn RadioList(
dirty.get() && matches!(error.get(), ValidationState::Invalid(_)) dirty.get() && matches!(error.get(), ValidationState::Invalid(_))
}); });
let input_state = InputState::new( let input_state = RwSignal::new(InputState::new(
data_member.get_untracked(), data_member.get_untracked(),
error.clone(), error.clone(),
dirty.clone() dirty.clone()
); ));
validation_context.validators.update(|list| list.push(RwSignal::new(input_state))); validation_context.validators.update(|list| list.push(input_state));
on_cleanup(move || {
validation_context.validators.update(|list| {
list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name)
});
});
//#endregion //#endregion
//#region Render Logic //#region Render Logic
+8 -2
View File
@@ -53,12 +53,18 @@ pub fn Select(
let dirty = RwSignal::new(false); let dirty = RwSignal::new(false);
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_))); let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
let input_state = InputState::new(data_member.get(), error.clone(), dirty.clone()); let input_state = RwSignal::new(InputState::new(data_member.get(), error.clone(), dirty.clone()));
let validation_context = use_validation_context() let validation_context = use_validation_context()
.expect("Could not find FormValidationRegistry context"); .expect("Could not find FormValidationRegistry context");
validation_context.validators.update(|list| list.push(RwSignal::new(input_state))); validation_context.validators.update(|list| list.push(input_state));
on_cleanup(move || {
validation_context.validators.update(|list| {
list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name)
});
});
//#endregion //#endregion
//#region Event Logic //#region Event Logic
+64 -4
View File
@@ -15,6 +15,8 @@ use crate::common::{
use leptos::prelude::ServerFnError; use leptos::prelude::ServerFnError;
use leptos::server; use leptos::server;
static COMPARISONS_TABLE: &str = "leptos-comparison-test";
/// Helper function to create all sports comparisons. Only used to create initial /// Helper function to create all sports comparisons. Only used to create initial
/// comparison data, and should be removed upon completion of a comparison editor page. /// comparison data, and should be removed upon completion of a comparison editor page.
#[allow(unused)] #[allow(unused)]
@@ -188,6 +190,64 @@ fn create_sports_comparisons() -> Vec<ComparisonData> {
] ]
} }
fn create_major_comparison(
id: impl Into<String>,
display_text: impl Into<String>,
) -> ComparisonData {
let display = display_text.into();
ComparisonData::new(
id,
"major",
ComparisonType::Text(TextComparison::Contains),
ValueType::String(Some(display.clone())),
"Majors",
display,
)
}
fn create_major_comparisons() -> Vec<ComparisonData> {
let display_texts = vec![
"Music",
"Education",
"Special Education",
"Speech Pathology",
"School Psychology",
"School Counseling",
"Occupational Therapy",
"Physical Therapy",
"Nursing",
"Allied Health",
"Fine/Performing Arts",
"Writing/Communication",
"History",
"Government",
"Political Science",
"Social Work",
"Sports Medicine",
"Athletic Training",
"Horticulture",
"Conservation Studies",
"Ecology",
"Environmental Studies",
"Urban Planning",
"Landscaping",
"Legal Studies",
"Criminal Justice",
];
let ids = display_texts
.iter()
.map(|text|
format!("{}_{}", "major", text.to_lowercase().replace(" ", "_").replace("/", "_"))
)
.collect::<Vec<String>>();
ids.iter().zip(display_texts).map(|(id, display_text)| {
create_major_comparison(id, display_text)
})
.collect::<Vec<ComparisonData>>()
}
#[allow(unused)] #[allow(unused)]
fn make_comp_list() -> Vec<ComparisonData> { fn make_comp_list() -> Vec<ComparisonData> {
let gpa_3 = ComparisonData::new( let gpa_3 = ComparisonData::new(
@@ -296,7 +356,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
); );
let mut sports_comps = create_sports_comparisons(); let mut sports_comps = create_sports_comparisons();
let mut major_comps = Vec::<ComparisonData>::new(); let mut major_comps = create_major_comparisons();
let mut comp_list = vec![ let mut comp_list = vec![
gpa_3, gpa_3,
@@ -328,7 +388,7 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
for comparison in comp_list { for comparison in comp_list {
if let Err(err) = client if let Err(err) = client
.put_item() .put_item()
.table_name("leptos-comparison-test") .table_name(COMPARISONS_TABLE)
.set_item(Some(serde_dynamo::to_item(&comparison)?)) .set_item(Some(serde_dynamo::to_item(&comparison)?))
.send() .send()
.await .await
@@ -351,7 +411,7 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
// id and display text, but we'll return the whole thing. // id and display text, but we'll return the whole thing.
match client match client
.scan() .scan()
.table_name("leptos-comparison-test") .table_name(COMPARISONS_TABLE)
.send() .send()
.await .await
{ {
@@ -377,7 +437,7 @@ pub async fn get_comparisons_categorized() -> Result<HashMap<String, Vec<Compari
match client match client
.scan() .scan()
.table_name("leptos-comparison-test") .table_name(COMPARISONS_TABLE)
.send() .send()
.await .await
{ {
+8 -6
View File
@@ -17,6 +17,8 @@ use crate::common::ExpandableInfo;
use leptos::prelude::ServerFnError; use leptos::prelude::ServerFnError;
use leptos::server; use leptos::server;
static SCHOLARSHIPS_TABLE: &str = "leptos-scholarship-test";
#[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")] #[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")]
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> { pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
@@ -26,7 +28,7 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
log!("Getting scholarship info using id {:?}", id); log!("Getting scholarship info using id {:?}", id);
match client match client
.get_item() .get_item()
.table_name("leptos-scholarship-test") .table_name(SCHOLARSHIPS_TABLE)
.key("subject", AttributeValue::S(id.clone())) .key("subject", AttributeValue::S(id.clone()))
.send() .send()
.await .await
@@ -58,7 +60,7 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
match client match client
.put_item() .put_item()
.table_name("leptos-scholarship-test") .table_name(SCHOLARSHIPS_TABLE)
.set_item(Some(serde_dynamo::to_item(&info)?)) .set_item(Some(serde_dynamo::to_item(&info)?))
.send() .send()
.await .await
@@ -77,7 +79,7 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
log!("Getting all scholarship info"); log!("Getting all scholarship info");
match client match client
.scan() .scan()
.table_name("leptos-scholarship-test") .table_name(SCHOLARSHIPS_TABLE)
.send() .send()
.await .await
{ {
@@ -104,7 +106,7 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
match client match client
.scan() .scan()
.table_name("leptos-scholarship-test") .table_name(SCHOLARSHIPS_TABLE)
.expression_attribute_values(":id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?) .expression_attribute_values(":id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?)
.filter_expression("provider_id = :id") .filter_expression("provider_id = :id")
.send() .send()
@@ -139,7 +141,7 @@ pub async fn register_scholarship(provider_id: String, scholarship_name: String)
let ser_item = serde_dynamo::to_item(&item)?; let ser_item = serde_dynamo::to_item(&item)?;
match client match client
.put_item() .put_item()
.table_name("leptos-scholarship-test") .table_name(SCHOLARSHIPS_TABLE)
.set_item(Some(ser_item)) .set_item(Some(ser_item))
.condition_expression("attribute_not_exists(subject)") .condition_expression("attribute_not_exists(subject)")
.send() .send()
@@ -180,7 +182,7 @@ pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: St
// otherwise everyone can delete anyone else's scholarships. // otherwise everyone can delete anyone else's scholarships.
match client match client
.delete_item() .delete_item()
.table_name("leptos-scholarship-test") .table_name(SCHOLARSHIPS_TABLE)
.key("subject", AttributeValue::S(scholarship_id)) .key("subject", AttributeValue::S(scholarship_id))
.expression_attribute_values(":provider_id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?) .expression_attribute_values(":provider_id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?)
.condition_expression("provider_id = :provider_id") .condition_expression("provider_id = :provider_id")
+5 -2
View File
@@ -10,6 +10,9 @@ use crate::pages::utils::get_user_claims;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use crate::pages::utils::server_utils::create_dynamo_client; use crate::pages::utils::server_utils::create_dynamo_client;
#[cfg(feature = "ssr")]
static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
#[server] #[server]
async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> { async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
use aws_sdk_dynamodb::types::AttributeValue; use aws_sdk_dynamodb::types::AttributeValue;
@@ -21,7 +24,7 @@ async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>,
match client match client
.get_item() .get_item()
.table_name("leptos-provider-contacts") .table_name(PROVIDER_CONTACT_TABLE)
.key("subject", AttributeValue::S(id)) .key("subject", AttributeValue::S(id))
.send() .send()
.await .await
@@ -58,7 +61,7 @@ async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueTyp
match client match client
.put_item() .put_item()
.table_name("leptos-provider-contacts") .table_name(PROVIDER_CONTACT_TABLE)
.set_item(Some(item)) .set_item(Some(item))
.send() .send()
.await .await
+14 -8
View File
@@ -416,8 +416,10 @@ fn ScholarshipForm(
if let Some(Ok(scholarship)) = scholarship_info.get() { if let Some(Ok(scholarship)) = scholarship_info.get() {
// Get the default chips data. // Get the default chips data.
let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") { let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") {
log!("Found requirements map: {:?}", map);
map.clone() map.clone()
} else { } else {
log!("Couldn't find requirements map in scholarship info. Using empty map.");
HashMap::new() HashMap::new()
}; };
@@ -464,15 +466,15 @@ fn ScholarshipForm(
let gpa_ids = Signal::derive(move || get_comp_ids("GPA Limits")); let gpa_ids = Signal::derive(move || get_comp_ids("GPA Limits"));
let gpa_text = Signal::derive(move || get_comp_text("GPA Limits")); let gpa_text = Signal::derive(move || get_comp_text("GPA Limits"));
// We'll collect the scholarship's name, num_awards, amount_per_award, total_awards, let major_ids = Signal::derive(move || get_comp_ids("Majors"));
// fafsa_required, award_to, transcript_required, recipient_selection, essay_requirement (and prompt) let major_text = Signal::derive(move || get_comp_text("Majors"));
// award_night_remarks
let mut toasts = expect_context::<ToastContext>(); let mut toasts = expect_context::<ToastContext>();
let on_submit = move |_| { let on_submit = move |_| {
let mut info = ExpandableInfo::new(scholarship_id.get().unwrap_or_default()); let mut info = ExpandableInfo::new(scholarship_id.get().unwrap_or_default());
info.data = form_data.get(); info.data = form_data.get();
info.data.insert("requirements".to_string(), ValueType::Map(Some(chips_data.get())));
log!("Map values: {:?}", info.data); log!("Map values: {:?}", info.data);
submit_action.dispatch(CreateScholarshipInfo { submit_action.dispatch(CreateScholarshipInfo {
@@ -518,11 +520,6 @@ fn ScholarshipForm(
Ok(_) => { Ok(_) => {
Either::Left( Either::Left(
view! { view! {
<Row>
<h1 class="text-3xl font-bold">
"Scholarship Info Form (test)"
</h1>
</Row>
<Row> <Row>
<OutlinedTextField <OutlinedTextField
label="Scholarship Name" label="Scholarship Name"
@@ -646,6 +643,15 @@ fn ScholarshipForm(
displayed_text=res_text displayed_text=res_text
/> />
</Row> </Row>
<Row>
<ChipsList
label="Majors"
data_member="major"
data_map=chips_data
values=major_ids
displayed_text=major_text
/>
</Row>
<Row> <Row>
<ChipsList <ChipsList
label="Additional Eligibility Factors" label="Additional Eligibility Factors"