use std::collections::HashMap; use leptos::either::Either; use leptos::html::Dialog; use leptos::logging::log; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; use leptos_router::hooks::{use_navigate, use_params}; use crate::common::{ComparisonData, ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType}; use crate::components::{ActionButton, Banner, ChipsList, Header, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm}; use super::UnauthenticatedPage; use crate::utils::get_user_claims; use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized}; /// # Scholarship Info Page /// This page will handle creating or editing scholarship information given a specific /// scholarship ID and scholarship provider subject number. The form itself will only /// track a single scholarship (for now) and so selection of what scholarship will be edited /// must be handled *before* navigation to this page. #[component] pub fn ScholarshipInfoPage() -> impl IntoView { // Changes to this signal update the scholarship list. let list_refresh = RwSignal::new(0); let url_params = use_params::(); let scholarship_id = Memo::new(move |_| { url_params.read() .as_ref() .ok() .and_then(|params| params.id.clone()) }); // let create_test_comps = ServerAction::::new(); let on_delete = move || { let navigate = use_navigate(); navigate("/providers/scholarships", Default::default()); }; // Remember that we only want to show the page after we've got authentication information // and if there's data for us to use. view! {
} } /// # Scholarship List Component /// /// This component should always be wrapped in an Authenticated component. Assuming that a provider /// is authenticated, this component will get all the scholarships that this provider owns and /// will display them in a list, giving them the option to edit, delete, or create new scholarships. /// /// Example usage: /// ``` /// let refresh = RwSignal::new(0); /// /// view! { /// /// /// /// /// /// } /// ``` #[component] fn ScholarshipList( /// A signal that refreshes the list when changed. Most times, we don't need to worry about the /// user reloading our page 2 billion times. #[prop()] refresh_token: RwSignal, /// A callback that is run on successful server-side deletion of an item. #[prop(into)] on_delete: Callback<()> ) -> impl IntoView { // List state let pending_delete = RwSignal::new(None::); let delete_dialog_ref = NodeRef::::new(); // Get scholarship provider information from authentication. This element will not display // unless that information is available. let user_claims = get_user_claims(); let provider_id = Memo::new(move |_| user_claims.get() .as_ref() .map(|info| info.claims.subject.clone()) ); // Register scholarship server resource let scholarships = Resource::new( move || { (refresh_token.get(), provider_id.get()) }, |(_, provider_id)| async move { log!("Fetching from scholarship API."); let Some(provider_id) = provider_id else { return Ok(Vec::new()); }; // Get the scholarships get_provider_scholarships(provider_id).await } ); // Register scholarship creation/deletion actions let create_action = ServerAction::::new(); let delete_action = ServerAction::::new(); // Button handlers let create_on_click = move |_| { create_action.dispatch(RegisterScholarship { provider_id: provider_id.get().unwrap() }); }; let on_item_delete = move |s| { pending_delete.set(Some(s)); }; // Register scholarship creation effect Effect::new(move || { // We want to wait for the action to complete first, but then find whether the request // succeeded. if let Some(Ok(_)) = create_action.value().get() { // Refetch and clear. We could alter this slightly to get the new element and // update the list on the client side only (no refetch). We also want to set the name // of the new scholarship before it's created on the server. scholarships.refetch(); create_action.clear(); } }); // Register scholarship deletion effects let modal_disabled = Signal::derive(move || delete_action.pending().get()); Effect::new(move || { if let Some(Ok(_)) = delete_action.value().get() { scholarships.refetch(); pending_delete.set(None); delete_action.clear(); on_delete.run(()); } }); // Register scholarship pending delete effects Effect::new(move || { if let Some(dialog) = delete_dialog_ref.get() { if pending_delete.get().is_some() { // Find the modal dialog and show it. Ask for the user's confirmation. Afterward, // take the correct action. dialog.show_modal() .expect("Couldn't show dialog. Is it already open?"); } else { dialog.close(); } } }); let modal_on_click_continue = move |_| { let subject = pending_delete .get() .map(|s| s.subject.clone()) .unwrap_or_default(); log!("Deleting scholarship with subject {}", subject); delete_action .dispatch(DeleteProviderScholarship { scholarship_id: subject, provider_id: provider_id.get().unwrap_or_default(), }); }; view! {

"Confirm Deletion"

"Are you sure you want to delete " {move || { pending_delete .get() .map(|s| { s.data .get("name") .unwrap_or(&ValueType::String(None)) .as_string() .unwrap_or_default() .unwrap_or("this unnamed scholarship".to_string()) }) }} "?"

"Yes" "No"

"Scholarships"

{move || { scholarships .get() .map(|result| { match result { Ok(list) => { let views = list .iter() .map(|entry| { view! { } }); Either::Left(views.collect_view()) } Err(err) => { Either::Right( view! {

"Failed to load scholarship list: "{err.to_string()}

}, ) } } }) .collect_view() }}
"Create New"
} } /// # Scholarship List Entry Component /// /// Displays a single scholarship within the [`ScholarshipList`] component. /// /// Example usage (within the [`ScholarshipList`]): /// ``` /// view! { /// /// } /// ``` #[component] fn ScholarshipListEntry( /// The scholarship information. This element will attempt to get the name of the scholarship /// using its `name` field, and also utilizes the `subject` field. #[prop()] scholarship: ExpandableInfo, /// A [`Callback`] that runs upon successful deletion of the scholarship. #[prop(into)] on_delete: Callback, ) -> impl IntoView { let navigate = use_navigate(); let user_claims = get_user_claims(); let _user_id = Memo::new(move |_| { user_claims.get() .map(|info| info.claims.subject.clone()) }); // Get the values that we need out of the info. We just need the subject and the name of // the scholarship. let name = scholarship.data.get("name") .unwrap_or(&ValueType::String(None)) .as_string().unwrap_or_default() .unwrap_or("Unnamed Scholarship".to_string()); // Button click handlers let edit_click = { let subject = scholarship.subject.clone(); move |_| { let path = format!("/providers/scholarships/{}", subject); navigate(path.as_str(), Default::default()); } }; view! {
{name}
"Edit"
"Delete"
} } /// # Scholarship Edit Form Component /// /// Displays all information about a scholarship and allows the provider to edit that information. /// All information is only saved when the submit button is clicked. /// /// The component will call the `on_submit` callback when the API returns an `Ok` result. In the /// event that the API call fails, it will not call anything. This is mainly used to allow updates /// to neighboring components from the parent. /// /// Example usage: /// ``` /// view! { /// /// } /// ``` #[component] fn ScholarshipForm( /// The ID of the scholarship to edit. The form will handle getting all information about the /// scholarship and submitting new information. #[prop(into)] scholarship_id: Signal>, /// A callback that is run when the form successfully submits new data. In the event that the /// submission fails, this function is not run. #[prop(into)] on_submit: Callback<()> ) -> impl IntoView { //#region Form Data Setup let scholarship_info = Resource::new( move || scholarship_id.get(), async move |opt_id| { if let Some(id) = opt_id { get_scholarship_info(id).await } else { Err(ServerFnError::new("Cannot get from server with no ID.")) } } ); let form_data = RwSignal::new(HashMap::new()); let chips_data = RwSignal::new(HashMap::new()); Effect::new(move || { if let Some(Ok(scholarship)) = scholarship_info.get() { // Get the default chips data. let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") { log!("Found requirements map: {:?}", map); map.clone() } else { log!("Couldn't find requirements map in scholarship info. Using empty map."); HashMap::new() }; chips_data.set(chips_default); form_data.set(scholarship.data); } }); //#endregion //#region Comparison Logic let comparison_lists = Resource::new( move || scholarship_id.get().is_some(), async move |_| { get_comparisons_categorized().await } ); let comparison_lists_sorted = Signal::derive(move || { if let Some(Ok(map)) = comparison_lists.get() { let mut map = map.clone(); map .into_iter() .map(|(cat, mut list)| { list.sort_by_key(|comp| comp.display_text.clone()); (cat, list) }) .collect::>>() } else { HashMap::new() } }); let get_comp_category = move |category: &str| { comparison_lists_sorted.get() .get(category) .cloned() .unwrap_or(Vec::new()) }; let get_comp_ids = move |category: &str| { get_comp_category(category) .into_iter() .map(|comp| comp.id) .collect::>() }; let get_comp_text = move |category: &str| { get_comp_category(category) .into_iter() .map(|comp| comp.display_text) .collect::>() }; // Get comparison lists. let sports_ids = Signal::derive(move || get_comp_ids("Sports Participation")); let sports_text = Signal::derive(move || get_comp_text("Sports Participation")); let misc_ids = Signal::derive(move || get_comp_ids("Additional Eligibility Factors")); let misc_text = Signal::derive(move || get_comp_text("Additional Eligibility Factors")); let comm_ids = Signal::derive(move || get_comp_ids("Community Service")); let comm_text = Signal::derive(move || get_comp_text("Community Service")); let res_ids = Signal::derive(move || get_comp_ids("Residency")); let res_text = Signal::derive(move || get_comp_text("Residency")); let gpa_ids = Signal::derive(move || get_comp_ids("GPA Limits")); let gpa_text = Signal::derive(move || get_comp_text("GPA Limits")); 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 |_| { let mut info = ExpandableInfo::new(scholarship_id.get().unwrap_or_default()); info.data = form_data.get(); info.data.insert("requirements".to_string(), ValueType::Map(Some(chips_data.get()))); log!("Map values: {:?}", info.data); submit_action.dispatch(CreateScholarshipInfo { info }); }; Effect::watch( move || submit_action.value().get(), move |value, _, _| { log!("Running submit_action effect"); let Some(result) = value else { return; }; let toast = match result { Ok(_) => Toast::new() .id(uuid::Uuid::new_v4()) .header("Submission Successful") .msg("You can go back or continue editing your responses."), Err(err) => Toast::new() .id(uuid::Uuid::new_v4()) .header("Submission Failed") .msg(err.to_string()) }; untrack(move || { submit_action.clear(); toasts.toast(toast); }); }, false ); //#endregion view! { "Choose a scholarship from the right, or create a new one."

} } > {move || { scholarship_info .get() .map(|res_scholarship| { match res_scholarship { Ok(_) => { Either::Left( view! {
}, ) } Err(err) => { Either::Right( view! {

"Error while getting scholarship: "{err.to_string()}

}, ) } } }) .collect_view() }} }.into_any() }