use leptos::either::Either; use leptos::logging::log; use leptos::prelude::*; use leptos::reactive::spawn_local; use leptos_oidc::{AuthLoaded, Authenticated}; use leptos_router::hooks::{use_navigate, use_params}; use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType}; use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row}; use super::UnauthenticatedPage; use traits::{AsReactive, ReactiveCapture}; use crate::common::ComparisonData; use crate::pages::utils::get_user_claims; use super::api::{get_comparison_info, get_provider_scholarships, get_scholarship_info, delete_provider_scholarship, CreateScholarshipInfo, CreateTestComparisons, RegisterScholarship}; /// # 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 { 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(); // 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. /// /// It does not accept any props, and instead pulls the provider's information from the surrounding /// environment. /// /// Example usage: /// ``` /// view! { /// /// /// /// /// /// } /// ``` #[component] fn ScholarshipList() -> impl IntoView { // 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 || provider_id.get(), async move |provider_id| { 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(); // Button handlers let create_on_click = move |_| { // In the future, we'd actually like to create/show a dialog that gets the // desired name of the scholarship. // After the request is sent, we'll handle it in an effect. create_action.dispatch(RegisterScholarship { provider_id: provider_id.get().unwrap(), scholarship_name: "Testing Scholarship".to_string() }); }; // 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. scholarships.refetch(); create_action.clear(); } }); view! {

"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("".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()); } }; let delete_click = { let subject = scholarship.subject.clone(); move |_| { // Delete the scholarship. let subject = subject.clone(); log!("Deleting scholarship with ID {:?}", subject); if let Some(user_id) = user_id.get() { spawn_local(async move { match delete_provider_scholarship( user_id, subject ).await { Ok(_) => on_delete.run(()), _ => {} } }); } } }; view! {
{name}
"Edit"
"Delete"
} } #[component] fn ScholarshipForm( #[prop(into)] scholarship_id: Signal> ) -> impl IntoView { let submit_status = RwSignal::new(SubmitStatus::Idle); let elements_disabled = Signal::derive(move || { match submit_status.get() { SubmitStatus::Sending => true, _ => false } }); let result_msg = Signal::derive(move || { match submit_status.get() { SubmitStatus::Idle => "".into(), SubmitStatus::Sending => "Sending submission...".into(), SubmitStatus::Success => "Success".into(), SubmitStatus::Error(msg) => format!("Error: {}", msg), } }); let submit_action = ServerAction::::new(); 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), Err(err) => submit_status.set(SubmitStatus::Error(err.to_string())) } } }); let comparison_list: Resource> = Resource::new( move || scholarship_id.get(), // There's no input to this function. async move |_| { get_comparison_info().await .unwrap_or_else(|e| { log!("Failed to get comparison info: {:?}", e); Vec::new() }) } ); 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 comparison_ids = Memo::new(move |_| { comparison_list.get().map(|list| list.iter() .map(|comp| comp.clone().id) .collect::>() ).unwrap_or_default() }); let comparison_text = Memo::new(move |_| { comparison_list.get().map(|list| list.iter() .map(|comp| comp.clone().display_text) .collect::>() ).unwrap_or_default() }); // We'll collect the scholarship's name, num_awards, amount_per_award, total_awards, // fafsa_required, award_to, transcript_required, recipient_selection, essay_requirement (and prompt) // award_night_remarks view! { "Choose a scholarship from the right, or create a new one."

} > {move || { scholarship_info.get() .map(|res_scholarship| { match res_scholarship { Ok(scholarship) => { let reactive_info = scholarship.as_reactive(); Either::Left(view! {

"Scholarship Info Form (test)"

"Submit"

{result_msg}

}) } Err(err) => { Either::Right(view! {

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

}) } } }).collect_view() }}
} }