use crate::common::{ ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType, }; use crate::components::{ActionButton, Banner, DataDisplay, Loading}; use crate::pages::UnauthenticatedPage; use crate::pages::api::get_comparison_info; use crate::pages::api::students::{ GetStudentFiles, GetStudentPdf, get_student_data, provider_get_all_input_files, }; use base64::Engine; use leptos::ev::{Event, MouseEvent}; use leptos::html::Dialog; use leptos::logging::debug_log; use leptos::prelude::*; use leptos::wasm_bindgen::JsCast; use leptos::web_sys; use leptos::web_sys::{HtmlAnchorElement, ScrollBehavior}; use leptos_icons::Icon; use leptos_oidc::{AuthLoaded, AuthSignal, Authenticated}; use leptos_router::components::Outlet; use leptos_router::hooks::use_navigate; use std::collections::HashMap; #[component] pub fn ApplicantsPageFallback() -> impl IntoView { view! {

"Scholarship Applicants"

"Please select a scholarship from the left to view the eligible applicants."

} } #[component] pub fn ApplicantsPageShell() -> impl IntoView { // This shell needs to simply contain everything that will stay consistent across paths. // The path will change to contain each scholarship's id, which will directly affect the content // shown in a subroute for this page. // As a result, this page likely only needs to contain the banner and the list of scholarships. // The content will be shown within the right side, but only as a result of some subroutes, // each of which is still considered a single page. view! {
} } #[component] fn ApplicantsScholarshipList() -> impl IntoView { use crate::common::ExpandableInfo; #[server] async fn get_provider_scholarships( access_token: String, ) -> Result, ServerFnError> { use crate::common::{COGNITO_REGION, STUDENT_PROVIDER_POOL_ID}; use crate::pages::api::SCHOLARSHIPS_TABLE; use crate::pages::api::tokens::validate_and_get_token_info; let claims = validate_and_get_token_info( access_token, STUDENT_PROVIDER_POOL_ID.to_string(), COGNITO_REGION.to_string(), ) .await?; // Get the information from the database. let client = crate::utils::server::create_dynamo_client().await; let output = client .scan() .table_name(SCHOLARSHIPS_TABLE) .expression_attribute_values( ":id", serde_dynamo::to_attribute_value(ValueType::String(Some(claims.subject)))?, ) .filter_expression("provider_id = :id") .send() .await? .items .unwrap_or_default() .into_iter() .filter_map(|item| serde_dynamo::from_item(item).ok()) .collect::>(); Ok(output) } // Use access token for user identification let auth = expect_context::(); let access_token = Memo::new(move |_| auth.with(|auth| auth.authenticated().map(|a| a.access_token()))); // We need to get the scholarships from the API. We don't need the whole scholarship, just the // name and ID. let trigger = Trigger::new(); let scholarships_res = Resource::new( move || (trigger.track(), access_token.get()), move |(_, access_token)| async move { get_provider_scholarships(access_token.unwrap_or_default()).await }, ); let navigate = use_navigate(); // This closure returns another closure that's using the correct scholarship ID. // It avoids having to define it within the Transition component. It's wrapped in a Callback // because this makes it Send + Sync. let on_view_click = Callback::new(move |scholarship_id: String| { let path = format!("/providers/applicants/{}", scholarship_id); navigate(&path, Default::default()); }); view! {

"Scholarships"

{move || { scholarships_res .get() .map(|items_res| { let items = match items_res { Ok(items) => items, Err(e) => { return view! {
{format!("Couldn't get scholarships: {}", e.to_string())}
} .into_any(); } }; view! { } .into_any() }) .collect_view() }}
} } #[component] fn ApplicantsScholarshipEntry( #[prop()] item: ExpandableInfo, #[prop(into)] on_view_click: Callback, ) -> impl IntoView { // Store the item's subject. let subject = StoredValue::new(item.subject); let scholarship_name = Memo::new(move |_| { item.data .get("name") .map(|name| name.as_string().ok().flatten()) .flatten() .unwrap_or("".to_string()) }); let on_click = move |_| { on_view_click.run(subject.get_value()); }; view! {
{scholarship_name}
"View Applicants"
} } #[component] pub fn ApplicantsStudentList() -> impl IntoView { use crate::pages::api::get_scholarship_info; use crate::pages::api::students::provider_get_completed_students; use leptos_router::hooks::use_params; use leptos_router::params::Params; /// The parameters for the page that displays this component. This may be moved to the page-level /// rather than the component-level. #[derive(Params, PartialEq)] struct StudentListParams { scholarship_id: Option, } // This is in charge of finding all student information and running the comparisons. // It will then display all the eligible students. let params = use_params::(); let auth = expect_context::(); let access_token = Memo::new(move |_| auth.with(|a| a.authenticated().map(|a| a.access_token()))); let scholarship_id = Memo::new(move |_| { params .read() .as_ref() .ok() .and_then(|params| params.scholarship_id.clone()) }); #[server] async fn get_eligibility_info( token: String, scholarship_id: String, ) -> Result< ( HashMap>, ExpandableInfo, Vec, ), ServerFnError, > { let (students, scholarships, requirements_list) = tokio::join!( provider_get_completed_students(token), get_scholarship_info(scholarship_id), get_comparison_info() ); Ok((students?, scholarships?, requirements_list?)) } let resource = Resource::new( move || (access_token.get(), scholarship_id.get()), move |(access_token, scholarship_id)| async move { let Some(token) = access_token else { return Err(ServerFnError::new("Couldn't find access token")); }; let Some(scholarship_id) = scholarship_id else { return Err(ServerFnError::new("Couldn't find scholarship ID")); }; get_eligibility_info(token, scholarship_id).await }, ); view! {
{move || { resource .get() .map(|result| { match result { Ok((students, scholarship, requirements_list)) => { view! {

"Eligible Students"

"Click on a student's name to view their information and download any requested files."
"If your scholarship requires a transcript, please send an email to Sara Smith at " "ssmith@region15.org" "."
} .into_any() } Err(e) => { view! {
"Error while getting eligible students: "{e.to_string()}
} .into_any() } } }) }}
} } #[component] fn ApplicantsStudentListView( #[prop(into)] access_token: Signal>, #[prop(into)] students: Signal>>, #[prop(into)] scholarship: Signal, #[prop(into)] requirements: Signal>, ) -> impl IntoView { let eligible_students = RwSignal::new(HashMap::new()); let error_msg = RwSignal::new(String::new()); // let (current_student_id, set_current_student_id) = query_signal::("student_id"); let current_student_id = RwSignal::new(None); let navigate_student = Callback::new(move |id: String| { let options = web_sys::ScrollToOptions::new(); options.set_top(0.0); options.set_behavior(ScrollBehavior::Smooth); if let Some(window) = web_sys::window() { window.scroll_to_with_scroll_to_options(&options); } current_student_id.set(Some(id)); }); let dialog_visible = Signal::derive(move || !current_student_id.get().unwrap_or_default().is_empty()); let on_dialog_close = Callback::new(move |_| { current_student_id.set(None); }); let fafsa_required = Memo::new(move |_| { let required = scholarship .get() .data .get("fafsa_required") .map(|v| v.as_string().ok().flatten()) .flatten() .unwrap_or_default(); required == "Yes" }); let essay_required = Memo::new(move |_| { let prompt = scholarship .get() .data .get("essay_prompt") .map(|v| v.as_string().ok().flatten()) .flatten() .unwrap_or_default(); !prompt.is_empty() }); let fafsa_resource = Resource::new( move || (fafsa_required.get(), access_token.get()), move |(fafsa_required, access_token)| async move { // Only get this information if the FAFSA is required. Otherwise, return a blank result. let Some(access_token) = access_token else { return Err(ServerFnError::new("Couldn't find access token")); }; if !fafsa_required { return Ok(HashMap::new()); } provider_get_all_input_files( access_token, "financial_info".to_string(), "fafsa".to_string(), ) .await }, ); let essay_resource = Resource::new( move || (essay_required.get(), access_token.get()), move |(essay_required, access_token)| async move { // Only get this information if the scholarship requested it. Otherwise, return a blank // result. let Some(access_token) = access_token else { return Err(ServerFnError::new("Couldn't find access token")); }; if !essay_required { return Ok(HashMap::new()); } provider_get_all_input_files( access_token, "scholarship_essays".to_string(), scholarship.get().subject, ) .await }, ); fn get_eligible_students( students: HashMap>, scholarship: ExpandableInfo, requirements: Vec, essay_required: bool, fafsa_required: bool, fafsa_list: HashMap>, essay_list: HashMap>, ) -> Result>, Error> { // Get the full map of requirements. Each key contains a list of IDs. let scholarship_requirements = scholarship .data .get("requirements") .map(|val| val.as_map().ok()?) .flatten() .unwrap_or_default(); // Get the lists of requirements. Maps into string IDs and discards invalid ValueTypes. let requirement_lists = scholarship_requirements .into_iter() .filter_map(|(_, list_val)| { let list = list_val.as_list().ok().flatten()?; let string_list = list .into_iter() .filter_map(|v| v.as_string().ok().flatten()) .collect::>(); Some(string_list) }) .collect::>>(); // Resolve the requirement IDs. Discards all IDs that do not resolve. let resolved_requirements = requirement_lists .into_iter() .map(|list| { list.into_iter() .filter_map(|id| { requirements .iter() .cloned() .find(|requirement| requirement.id == id) }) .collect::>() }) .collect::>>(); // Check student eligibility. These are all students that have completed the demographics // forms. let eligible_students = students .into_iter() .filter_map(|(id, student)| { // The student needs to pass one requirement from each list. // The easiest ones to check first are whether the scholarship requires a student // essay and/or their fafsa information. If the student does not have these things, // they are skipped. if fafsa_required && !fafsa_list.contains_key(&id) { // Get the student files related to their fafsa and continue. Return false if not there. debug_log!( "Student with id {id} failed - FAFSA is required but was not found." ); return None; } if essay_required && !essay_list.contains_key(&id) { // Get the student's files related to their essays and continue. Return false if not there. debug_log!( "Student with id {id} failed - essay is required but was not found." ); return None; } let result = resolved_requirements.iter().all(|list| { if list.is_empty() { return true; } list.iter().any(|requirement| { let result = requirement.compare(&student).unwrap_or(false); if !result { debug_log!( "Student with id {id} failed - requirement with id {} failed.", requirement.id ); } result }) }); if result { Some((id, student)) } else { None } }) .collect::>>(); // debug_log!("Total number of eligible students: {}", eligible_students.len()); Ok(eligible_students) } Effect::new(move || { // We have to wait until we've gotten fafsa/essay requirements. debug_log!("Waiting on essay and FAFSA resources..."); if let (Some(fafsa_list), Some(essay_list)) = (fafsa_resource.get(), essay_resource.get()) { debug_log!( "Running eligibility check on {} students", students.get().len() ); let eligible_res = get_eligible_students( students.get(), scholarship.get(), requirements.get(), essay_required.get(), fafsa_required.get(), fafsa_list.unwrap_or_default(), essay_list.unwrap_or_default(), ); match eligible_res { Ok(list) => { debug_log!("Total number of eligible students: {}", list.len()); eligible_students.set(list); } Err(e) => error_msg.set(format!("Failed to get eligible students list: {e}")), } } }); // This is actually just a display of all the information from the students list. // As defined in the ApplicantsStudentList component, this will already be wrapped in a // flex container. view! {
"Error checking student eligibility: "{error_msg}
"Last Name"
"First Name"
{last_name}
{first_name}
} } />
} } #[component] pub fn StudentInformationDialog( #[prop(into)] scholarship_id: Signal, #[prop(into)] student_id: Signal>, #[prop(into)] visible: Signal, #[prop(into)] fafsa_required: Signal, #[prop(into)] essay_required: Signal, #[prop(into)] on_close: Callback<()>, ) -> impl IntoView { // This component needs to just display all student information. // Specifically, it needs to know what information to display, like whether the scholarship // requested an essay or financial information. let dialog_ref = NodeRef::::new(); let auth = expect_context::(); let access_token = Memo::new(move |_| auth.with(|auth| auth.authenticated().map(|a| a.access_token()))); //#region Schema Definitions let demographics_schema = StoredValue::new( SchemaNode::builder() .container(SchemaContainerStyle::Header) .header_style(SchemaHeaderStyle::MainHeader) .header("Demographics Information") .map() .child( "first_name", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("First Name") .string() .build(), ) .child( "last_name", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Last Name") .string() .build(), ) .child( "dob", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Date of Birth") .string() .build(), ) .child( "gender", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Gender") .string() .build(), ) .child( "phone_number", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Preferred Phone Number") .string() .build(), ) .child( "email", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Preferred Email Address") .string() .build(), ) .child( "street_address", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Street Address") .string() .build(), ) .child( "town", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Town") .string() .build(), ) .build(), ); let academics_schema = StoredValue::new( SchemaNode::builder() .header_style(SchemaHeaderStyle::MainHeader) .header("Academic Information") .map() .child( "unweighted_gpa", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Unweighted GPA") .number() .build(), ) .child( "weighted_gpa", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Weighted GPA") .number() .build(), ) .child( "sat_score", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Highest SAT Score") .number() .build(), ) .child( "act_score", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Highest ACT Score") .number() .build(), ) .child( "academic_honors", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Academic Honors") .string() .build(), ) .build(), ); let sports_schema = StoredValue::new( SchemaNode::builder() .header_style(SchemaHeaderStyle::MainHeader) .header("Sports Activities") .map_list() .primary_key("uuid") .item_template( SchemaNode::builder() .container(SchemaContainerStyle::Capsule) .header_style(SchemaHeaderStyle::Bold) .header("Sport") .map() .child( "sport_name", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Sport Name") .string() .build(), ) .child( "achievements", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Special Achievements") .string() .build(), ) .build(), ) .build(), ); let extracurricular_schema = StoredValue::new( SchemaNode::builder() .header_style(SchemaHeaderStyle::MainHeader) .header("Extracurricular Activities") .map() .child( "service_hours", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Total Number of Service Hours") .number() .build(), ) .child( "extracurricular", SchemaNode::builder() .map_list() .primary_key("uuid") .item_template( SchemaNode::builder() .container(SchemaContainerStyle::Capsule) .header_style(SchemaHeaderStyle::Bold) .header("Activity") .map() .child( "activity_name", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Activity Name") .string() .build(), ) .child( "num_hours", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Approximate Number of Hours Completed") .number() .build(), ) .child( "num_weeks", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Approximate Number of Weeks Participated") .number() .build(), ) .child( "special_involvement", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Special Involvement") .string() .build(), ) .build(), ) .build(), ) .build(), ); let work_exp_schema = StoredValue::new( SchemaNode::builder() .header_style(SchemaHeaderStyle::MainHeader) .header("Work Experience") .map_list() .item_template( SchemaNode::builder() .container(SchemaContainerStyle::Capsule) .header_style(SchemaHeaderStyle::Bold) .header("Experience") .map() .child( "job_title", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Job Title") .string() .build(), ) .child( "employer", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Employer") .string() .build(), ) .child( "start_date", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Start Date") .string() .build(), ) .child( "end_date", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("End Date") .string() .build(), ) .child( "num_hours", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Approximate Number of Hours") .number() .build(), ) .build(), ) .build(), ); let university_schema = StoredValue::new( SchemaNode::builder() .header_style(SchemaHeaderStyle::MainHeader) .header("University Information") .map() .child( "college_name", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("University Name") .string() .build(), ) .child( "college_city", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("University Street Address") .string() .build(), ) .child( "college_state", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("University State") .string() .build(), ) .child( "college_zip", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("University ZIP") .string() .build(), ) .child( "college_acceptance", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Has the student received an acceptance?") .string() .build(), ) .child( "major", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Chosen Major") .string() .build(), ) .child( "intended_career", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Intended Career") .string() .build(), ) .build(), ); let family_schema = StoredValue::new( SchemaNode::builder() .header_style(SchemaHeaderStyle::MainHeader) .header("Family Information") .map() .child( "num_children", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Total number of children in student's family") .number() .build(), ) .child( "num_children_college", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Total number of children currently attending college") .number() .build(), ) .child( "parent_one_name", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Parent/Guardian 1 Name") .string() .build(), ) .child( "parent_one_relationship", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Parent/Guardian 1 Relationship") .string() .build(), ) .child( "parent_one_occupation", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Parent/Guardian 1 Occupation") .string() .build(), ) .child( "parent_one_employer", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Parent/Guardian 1 Employer") .string() .build(), ) .child( "parent_two_name", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Parent/Guardian 2 Name") .string() .build(), ) .child( "parent_two_relationship", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Parent/Guardian 2 Relationship") .string() .build(), ) .child( "parent_two_occupation", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Parent/Guardian 2 Occupation") .string() .build(), ) .child( "parent_two_employer", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Parent/Guardian 2 Employer") .string() .build(), ) .build(), ); let specifics_schema = StoredValue::new( SchemaNode::builder() .header_style(SchemaHeaderStyle::MainHeader) .header("Extra Eligibility Information") .map() .child( "attend_bas", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Did the student attend BAS?") .string() .build(), ) .child( "middsouth_church", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Is the student a member of Midd-South Catholic Church?") .string() .build(), ) .child( "family_military_service", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header( "Does the student have a family member who has served in the US Military?", ) .string() .build(), ) .child( "youth_baseball", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Has the student participated in Pomperaug Youth Baseball?") .string() .build(), ) .child( "aquatic_club", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Has the student participated in the Panthers Aquatic Club?") .string() .build(), ) .child( "pea_member", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Does the student have a family member in the Region 15 PEA?") .string() .build(), ) .child( "music_program", SchemaNode::builder() .header_style(SchemaHeaderStyle::Bold) .header("Has the student participated in the PHS Music Program?") .string() .build(), ) .build(), ); //#endregion Effect::new(move || { if let Some(dialog) = dialog_ref.get() { let open = visible.get(); if open { let _ = dialog.show_modal(); debug_log!("Modal should be visible."); } else { dialog.close(); debug_log!("Closed modal."); } } }); // We want to get the student's uploaded files - only the ones that are relevant to the current // scholarship, meaning their essays and/or FAFSA by request. let get_student_files = ServerAction::::new(); let get_student_pdf = ServerAction::::new(); let get_buttons_disabled = Memo::new(move |_| get_student_files.pending().get() && get_student_pdf.pending().get()); let on_click_fafsa = move |_| { get_student_files.dispatch(GetStudentFiles { access_token: access_token.get().unwrap_or_default(), student_id: student_id.get().unwrap_or_default(), form_name: "financial_info".to_string(), question_id: "fafsa".to_string(), file_name_postfix: "FafsaInfo".to_string(), }); }; let on_click_essay = move |_| { debug_log!("Getting essay files"); get_student_files.dispatch(GetStudentFiles { access_token: access_token.get().unwrap_or_default(), student_id: student_id.get().unwrap_or_default(), form_name: "scholarship_essays".to_string(), question_id: scholarship_id.get(), file_name_postfix: "Essays".to_string(), }); }; let on_click_pdf = move |_| { debug_log!("Getting student PDF"); get_student_pdf.dispatch(GetStudentPdf { student_id: student_id.get().unwrap_or_default(), }); }; fn open_file(file_name: String, file: Vec) { let base64 = base64::engine::general_purpose::STANDARD.encode(file); let data_url = format!("data:application/zip;base64,{}", base64); // Create a base64 link and open it. let a: HtmlAnchorElement = document() .create_element("a") .expect("Failed to create anchor element") .dyn_into() .unwrap(); a.set_download(&*file_name); a.set_href(&data_url); document().body().unwrap().append_child(&a).unwrap(); a.click(); a.remove(); } Effect::new(move || { if let Some(Ok((file_name, file))) = get_student_files.value().get() { open_file(file_name, file); get_student_files.clear(); } }); // Copy and paste is bad, but it's a little short on time. Effect::new(move || { if let Some(Ok((file_name, file))) = get_student_pdf.value().get() { open_file(file_name, file); get_student_files.clear(); } }); view! {

"Student Information"

"Download FAFSA File(s)" "Download Essay File(s)" "Download Student Application"
{move || Suspend::new(async move { get_student_data( student_id.get().unwrap_or_default(), "demographics".to_string(), ) .await .map(|student_info| { view! { } }) })} {move || Suspend::new(async move { get_student_data( student_id.get().unwrap_or_default(), "academics".to_string(), ) .await .map(|student_info| { view! { } }) })} {move || Suspend::new(async move { get_student_data( student_id.get().unwrap_or_default(), "athletics".to_string(), ) .await .map(|student_info| { view! { } }) })} {move || Suspend::new(async move { get_student_data( student_id.get().unwrap_or_default(), "demographics".to_string(), ) .await .map(|student_info| { view! { } }) })} {move || Suspend::new(async move { get_student_data( student_id.get().unwrap_or_default(), "workexp".to_string(), ) .await .map(|student_info| { view! { } }) })} {move || Suspend::new(async move { get_student_data( student_id.get().unwrap_or_default(), "university".to_string(), ) .await .map(|student_info| { view! { } }) })} {move || Suspend::new(async move { get_student_data(student_id.get().unwrap_or_default(), "family".to_string()) .await .map(|student_info| { view! { } }) })} {move || Suspend::new(async move { get_student_data( student_id.get().unwrap_or_default(), "specifics".to_string(), ) .await .map(|student_info| { view! { } }) })}
} }