Compare commits

...
30 Commits
Author SHA1 Message Date
Lucas Vas 467f8c9020 Merge pull request #76 from R15-Software-Dev/chore/api-err-return
Make student eligibility return an error
2026-03-25 13:42:25 -04:00
alphafetus 909f33b044 chore: make student eligibility return an error on missing access token 2026-03-25 13:41:10 -04:00
Lucas Vas 44450f407c Merge pull request #75 from R15-Software-Dev/chore/close-providers
Close provider forms
2026-03-25 13:31:14 -04:00
Lucas Vas 70a86f4545 Merge pull request #74 from R15-Software-Dev/feat/file-input-errors
Create error message display for file inputs
2026-03-25 13:28:23 -04:00
alphafetus e6409cc423 chore: close provider forms 2026-03-25 13:20:58 -04:00
alphafetus 4e3161a05f feat: create error message display for file inputs 2026-03-25 13:17:43 -04:00
Lucas Vas 51dfa013df Merge pull request #73 from R15-Software-Dev/fix/blank-id-requests
Fix for blank IDs in submissions
2026-03-25 12:58:55 -04:00
alphafetus 1d41b6022e fix: hopeful fix for blank IDs in submissions 2026-03-25 12:39:50 -04:00
Lucas Vas f9d5b25436 Merge pull request #72 from R15-Software-Dev/fix/filtering-bugs
Check requirement return value on Ok variant
2026-03-23 12:05:22 -04:00
alphafetus 891e324186 fix: check requirement return value on Ok variant 2026-03-23 11:47:06 -04:00
Lucas Vas b502c2f96d Merge pull request #71 from R15-Software-Dev/fix/comparison-filtering
Fix comparison logic for filtering
2026-03-19 13:49:06 -04:00
alphafetus 681da8c326 fix: use correct comparison logic
Was originally short-circuiting on empty lists of requirements.
2026-03-19 13:37:11 -04:00
Lucas Vas 628b607074 Merge pull request #70 from R15-Software-Dev/fix/api-errors
Significantly improve logging for api requests
2026-03-18 12:30:19 -04:00
alphafetus 97be249826 fix: import ProvideErrorMetadata correctly 2026-03-18 12:16:44 -04:00
alphafetus e1bd472a36 feat: significantly improve logging for api requests 2026-03-18 12:08:41 -04:00
Lucas Vas dd19631e33 Merge pull request #69 from R15-Software-Dev/fix/comparison-changes
Small comparison changes
2026-03-18 10:38:26 -04:00
alphafetus a0b206a554 fix: do not display scholarships that are not completed 2026-03-12 10:41:32 -04:00
alphafetus e9b0452d38 feat: improve error messages for client side comparison check 2026-03-11 14:24:25 -04:00
Lucas Vas 448e8bbb38 Merge pull request #68 from R15-Software-Dev/fix/comparison-changes
Comparison and sports changes
2026-03-11 12:49:53 -04:00
alphafetus ae7a17ad92 chore: add volleyball option 2026-03-11 11:54:34 -04:00
alphafetus 750059e9ad fix: attended_bas checks correct data member 2026-03-11 11:53:50 -04:00
alphafetus 5a371ae574 chore: temporarily reopen provider forms (for real this time) 2026-03-11 11:53:05 -04:00
alphafetus d7f44463c6 chore: temporarily reopen provider forms 2026-03-11 11:33:30 -04:00
alphafetus 5872002538 feat: improve error messages in files api 2026-03-11 11:28:52 -04:00
alphafetus 957fc0c5eb chore: change title of form 2026-03-09 12:16:41 -04:00
Lucas Vas 8a2d5b0494 Merge pull request #67 from R15-Software-Dev/hotfix/66-minor-changes
Minor changes to student forms
2026-03-06 13:58:33 -05:00
alphafetus 07fae5878f feat: add toasts and reactive feedback to forms on submission
Users were not able to tell that they had submitted before this.
2026-03-06 13:46:35 -05:00
alphafetus def5c8696b chore: update intended major field 2026-03-06 13:23:51 -05:00
alphafetus 7c1e281749 chore: add due date reminder to home page 2026-03-06 13:22:28 -05:00
alphafetus 49bed2f1e1 fix: correct gender data member 2026-03-06 13:21:18 -05:00
21 changed files with 469 additions and 267 deletions

No files matched your search

+3 -3
View File
@@ -90,9 +90,9 @@ fn ProviderRoutes() -> impl MatchNestedRoutes + Clone {
<Route path=path!("/callback") view=AuthCallbackPage />
<Route path=path!("/profile") view=ProviderContactPage />
<Route path=path!("/scholarships") view=FormClosedPage />
// <ParentRoute path=path!("/scholarships") view=FormClosedPage>
// <Route path=path!(":id") view=FormClosedPage />
// <Route path=path!("") view=FormClosedPage />
// <ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage>
// <Route path=path!(":id") view=ScholarshipInfoPage />
// <Route path=path!("") view=ScholarshipInfoPage />
// </ParentRoute>
</ParentRoute>
}
+16 -4
View File
@@ -1,6 +1,6 @@
use crate::pages::api::files::{DeleteFile, upload_file};
use leptos::html::{Input, Label};
use leptos::logging::debug_log;
use leptos::logging::{debug_error, debug_log};
use leptos::prelude::*;
use leptos::web_sys::{File, FormData};
use leptos_icons::Icon;
@@ -28,6 +28,7 @@ pub fn FileDrop(
let file_name_list = RwSignal::new(Vec::new());
let hovering = RwSignal::new(false);
let error_msg = RwSignal::new(String::new());
let name = StoredValue::new(name);
let form_id = StoredValue::new(form_id);
let auth = expect_context::<AuthSignal>();
@@ -172,10 +173,18 @@ pub fn FileDrop(
existing_files
.get()
.map(|result| {
if let Ok(files) = result {
file_name_list.set(files);
match result {
Ok(files) => {
error_msg.set("".to_string());
file_name_list.set(files);
}
Err(err) => {
let msg = err.to_string();
debug_error!("Error while getting files: {}", msg);
error_msg.set(msg);
}
}
})
});
}}
<label
node_ref=zone_ref
@@ -194,6 +203,9 @@ pub fn FileDrop(
<div class="text-sm text-gray-400">"Or click to select a file"</div>
<input node_ref=input_ref type="file" class="hidden" on:change=on_input_change />
</label>
<Show when=move || !error_msg.get().is_empty()>
<div class="flex-1">{error_msg}</div>
</Show>
<For
each=move || file_name_list.get()
key=|file| file.clone()
+3
View File
@@ -111,6 +111,8 @@ pub fn ValidatedForm(
#[prop(into)] title: Signal<String>,
/// The description for the included header.
#[prop(optional, into)] description: Signal<String>,
/// A `Signal` that determines when the submit button should be disabled.
#[prop(optional, into)] disabled: Signal<bool>,
) -> impl IntoView {
let validators = RwSignal::new(vec![]);
@@ -154,6 +156,7 @@ pub fn ValidatedForm(
<Header title=title description=description />
{children()}
<ActionButton
disabled=disabled
on:click=move |_| {
log!("{:?}", can_submit.get());
}
+37 -44
View File
@@ -14,10 +14,9 @@ use crate::common::{
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
TextComparison, TextListComparison, ValueType,
};
use std::collections::HashMap;
use leptos::prelude::ServerFnError;
use leptos::server;
use std::collections::HashMap;
/// 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.
@@ -250,14 +249,18 @@ fn create_major_comparisons() -> Vec<ComparisonData> {
let ids = display_texts
.iter()
.map(|text|
format!("{}_{}", "major", text.to_lowercase().replace(" ", "_").replace("/", "_"))
)
.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)
})
ids.iter()
.zip(display_texts)
.map(|(id, display_text)| create_major_comparison(id, display_text))
.collect::<Vec<ComparisonData>>()
}
@@ -280,13 +283,11 @@ fn make_comp_list() -> Vec<ComparisonData> {
"Academic Information",
"Math SAT Score > 650",
);
let service_hours_20 = ComparisonData::new(
"service_hours_20",
"service_hours",
ComparisonType::Number(
NumberComparison::GreaterThanOrEqual
),
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
ValueType::Number(Some(20.to_string())),
"Community Service",
"20+ service hours",
@@ -295,9 +296,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
let service_hours_25 = ComparisonData::new(
"service_hours_25",
"service_hours",
ComparisonType::Number(
NumberComparison::GreaterThanOrEqual
),
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
ValueType::Number(Some(25.to_string())),
"Community Service",
"25+ service hours",
@@ -306,9 +305,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
let service_hours_30 = ComparisonData::new(
"service_hours_30",
"service_hours",
ComparisonType::Number(
NumberComparison::GreaterThanOrEqual
),
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
ValueType::Number(Some(30.to_string())),
"Community Service",
"30+ service hours",
@@ -334,7 +331,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
let attended_bas = ComparisonData::new(
"attended_bas",
"attended_bas",
"attend_bas",
ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())),
"Additional Eligibility Factors",
@@ -365,7 +362,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())),
"Additional Eligibility Factors",
"Participated in Pomperaug Youth Baseball"
"Participated in Pomperaug Youth Baseball",
);
let aquatic_club = ComparisonData::new(
@@ -374,25 +371,25 @@ fn make_comp_list() -> Vec<ComparisonData> {
ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())),
"Additional Eligibility Factors",
"Participated in Panthers Aquatic Club"
"Participated in Panthers Aquatic Club",
);
let pea_member = ComparisonData::new(
"pea_member",
"pea_member",
ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())),
"Additional Eligibility Factors",
"Has a Parent in the PEA"
"Has a Parent in the PEA",
);
let music_program = ComparisonData::new(
"music_program",
"music_program",
ComparisonType::Text(TextComparison::Matches),
ValueType::String(Some("Yes".to_string())),
"Sports Participation",
"PHS Music Program"
"PHS Music Program",
);
let mut sports_comps = create_sports_comparisons();
@@ -412,7 +409,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
youth_baseball,
aquatic_club,
pea_member,
music_program
music_program,
];
comp_list.append(&mut sports_comps);
@@ -438,6 +435,7 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
.await
{
let msg = err.message().unwrap_or("An unknown error occurred");
leptos::logging::error!("{}", msg);
return Err(ServerFnError::new(msg));
}
}
@@ -453,15 +451,9 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
// Query the database for all comparisons. The client is only going to use the
// id and display text, but we'll return the whole thing.
match client
.scan()
.table_name(COMPARISONS_TABLE)
.send()
.await
{
match client.scan().table_name(COMPARISONS_TABLE).send().await {
Ok(output) => {
if let Some(items) = output.items {
log!("Found comparisons from API: {:?}", items);
Ok(serde_dynamo::from_items(items)?)
} else {
Ok(vec![])
@@ -469,22 +461,19 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
}
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred");
leptos::logging::error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
}
#[server]
pub async fn get_comparisons_categorized() -> Result<HashMap<String, Vec<ComparisonData>>, ServerFnError> {
pub async fn get_comparisons_categorized()
-> Result<HashMap<String, Vec<ComparisonData>>, ServerFnError> {
let client = create_dynamo_client().await;
log!("Getting all comparisons from the database, by category.");
match client
.scan()
.table_name(COMPARISONS_TABLE)
.send()
.await
{
match client.scan().table_name(COMPARISONS_TABLE).send().await {
Ok(output) => {
if let Some(items) = output.items {
log!("Found comparisons, categorizing.");
@@ -492,23 +481,27 @@ pub async fn get_comparisons_categorized() -> Result<HashMap<String, Vec<Compari
let items: Vec<ComparisonData> = serde_dynamo::from_items(items)?;
let categorized = items.iter().fold(
HashMap::<String, Vec<ComparisonData>>::new(), |mut map, comp| {
// Load each comparison into a map, then return that map.
HashMap::<String, Vec<ComparisonData>>::new(),
|mut map, comp| {
// Load each comparison into a map, then return that map.
map.entry(comp.category.clone())
.and_modify(|vec| vec.push(comp.clone()))
.or_insert(vec![comp.clone()]);
map
}
},
);
Ok(categorized)
} else {
Err(ServerFnError::new("Couldn't find any comparisons."))
let msg = "Couldn't find any comparisons.";
leptos::logging::error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred");
leptos::logging::error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
+32 -32
View File
@@ -1,61 +1,60 @@
use std::collections::HashMap;
use leptos::logging::log;
use leptos::prelude::*;
use crate::common::DateInfo;
use leptos::logging::{error, log};
use leptos::prelude::*;
use std::collections::HashMap;
#[cfg(feature = "ssr")]
use super::DATES_TABLE;
#[cfg(feature = "ssr")]
use aws_sdk_dynamodb::{
types::{WriteRequest, PutRequest},
Client as DynamoClient
};
#[cfg(feature = "ssr")]
use crate::utils::server::create_dynamo_client;
#[cfg(feature = "ssr")]
use std::sync::Arc;
use aws_sdk_dynamodb::{
Client as DynamoClient,
error::ProvideErrorMetadata,
types::{PutRequest, WriteRequest},
};
#[server]
pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> {
// Serialize and put dates into the database.
let client = create_dynamo_client().await;
log!("Creating dates: {:?}", dates);
let requests = dates
.into_iter()
.map(|date| {
let item = serde_dynamo::to_item(&date)
.unwrap_or_default();
let put_request = PutRequest::builder()
.set_item(Some(item))
.build()
.expect("Couldn't build request, item is not present");
WriteRequest::builder().put_request(put_request).build()
.filter_map(|date| {
let item = serde_dynamo::to_item(&date).unwrap_or_default();
let put_request = PutRequest::builder().set_item(Some(item)).build().ok()?;
Some(WriteRequest::builder().put_request(put_request).build())
})
.collect::<Vec<WriteRequest>>();
let mut request_map = HashMap::new();
request_map.insert(DATES_TABLE.to_string(), requests);
client
.batch_write_item()
.set_request_items(Some(request_map))
.send()
.await
.map(|_| ())
.map_err(ServerFnError::from)
.map_err(|err| {
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
ServerFnError::new(msg)
})
}
#[server]
pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> {
log!("Getting important dates list");
let client = create_dynamo_client().await;
client
.scan()
.table_name(DATES_TABLE)
@@ -65,14 +64,15 @@ pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> {
if let Some(items) = output.items {
items
.into_iter()
.filter_map(|item| {
serde_dynamo::from_item::<_, DateInfo>(item)
.ok()
})
.filter_map(|item| serde_dynamo::from_item::<_, DateInfo>(item).ok())
.collect::<Vec<DateInfo>>()
} else {
Vec::new()
}
})
.map_err(ServerFnError::from)
.map_err(|err| {
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
ServerFnError::new(msg)
})
}
-1
View File
@@ -231,7 +231,6 @@ pub async fn get_scholarship_csv() -> Result<Vec<u8>, ServerFnError> {
writer.flush()?;
let result = writer.into_inner()?;
debug_log!("{:?}", String::from_utf8(result.clone()));
Ok(result)
}
+53 -18
View File
@@ -1,4 +1,5 @@
use leptos::logging::debug_log;
use leptos::leptos_dom::error;
use leptos::logging::debug_log;
use leptos::prelude::*;
use server_fn::codec::{MultipartData, MultipartFormData};
@@ -7,7 +8,11 @@ mod imports {
pub use super::super::{MAIN_TABLE_NAME, S3_BUCKET_NAME};
pub use crate::pages::api::tokens::validate_and_get_token_info;
pub use crate::utils::server::create_aws_config;
pub use aws_sdk_dynamodb::types::{AttributeValue, ReturnValue, Select};
pub use aws_sdk_dynamodb::{
error::ProvideErrorMetadata as _,
types::{AttributeValue, ReturnValue, Select},
};
pub use aws_sdk_s3::error::ProvideErrorMetadata as _;
}
#[server(input = MultipartFormData)]
@@ -57,6 +62,9 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
if input_name.is_empty() {
return Err(ServerFnError::new("Missing input file"));
}
if file_bytes.is_empty() {
return Err(ServerFnError::new("Missing file contents"));
}
let user_claims = validate_and_get_token_info(access_token).await?;
let subject = user_claims.subject;
@@ -75,7 +83,14 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
.send()
.await
.map(|_| ())
.map_err(|e| ServerFnError::new(format!("Couldn't put file to S3: {}", e.to_string())))?;
.map_err(|e| {
let msg = format!(
"Couldn't put file to S3: {}",
e.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})?;
// Besides uploading the file to S3, we also want to store a FILE entry in Dynamo.
// This will allow the server to quickly return a list of all files that a specific user has
@@ -101,11 +116,13 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
.item("file_key".to_string(), AttributeValue::S(key.clone()))
.send()
.await
.map_err(|e| {
ServerFnError::new(format!(
.map_err(|err| {
let msg = format!(
"Couldn't put file to Dynamo, file rolled back: {}",
e.to_string()
))
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
});
// If Dynamo fails, we want to handle the error by rolling back the file. Then, we return a failure.
@@ -116,11 +133,13 @@ pub async fn upload_file(data: MultipartData) -> Result<String, ServerFnError> {
.key(&key)
.send()
.await
.map_err(|e| {
ServerFnError::new(format!(
"Couldn't roll back S3 operation: {}",
e.to_string()
))
.map_err(|err| {
let msg = format!(
"Failed to rollback Dynamo entry: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})?;
return Err(err);
@@ -167,7 +186,14 @@ pub async fn delete_file(
.key(key)
.send()
.await
.map_err(ServerFnError::from);
.map_err(|err| {
let msg = format!(
"Failed to put file into S3: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
});
if let Err(err) = s3_result {
// Add the key back to Dynamo and return an error.
@@ -178,10 +204,12 @@ pub async fn delete_file(
.send()
.await
.map_err(|err| {
ServerFnError::new(format!(
"Couldn't roll back Dynamo operation: {}",
err.to_string()
))
let msg = format!(
"Failed to rollback Dynamo entry: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})?;
return Err(err);
@@ -230,5 +258,12 @@ pub async fn list_files(
})
.collect::<Vec<String>>()
})
.map_err(|e| ServerFnError::new(format!("Couldn't list files: {}", e.to_string())))
.map_err(|err| {
let msg = format!(
"Failed to get files: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})
}
+23 -12
View File
@@ -1,8 +1,8 @@
use leptos::prelude::*;
use leptos::logging::debug_log;
use std::collections::HashMap;
use crate::common::ValueType;
use leptos::logging::{debug_log, error};
use leptos::prelude::*;
use leptos::server_fn::codec::Json;
use crate::common::ValueType;
use std::collections::HashMap;
#[cfg(feature = "ssr")]
use crate::utils::server::{create_dynamo_client, into_attr_map};
@@ -12,8 +12,8 @@ static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
#[server]
pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
use aws_sdk_dynamodb::types::AttributeValue;
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
use aws_sdk_dynamodb::types::AttributeValue;
let client = create_dynamo_client().await;
@@ -31,29 +31,39 @@ pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueTyp
return Ok(HashMap::new());
};
let map = item.iter().map(|(key, val)| {
let val_type = ValueType::from(val);
let map = item
.iter()
.map(|(key, val)| {
let val_type = ValueType::from(val);
(key.clone(), val_type)
}).collect::<HashMap<String, ValueType>>();
(key.clone(), val_type)
})
.collect::<HashMap<String, ValueType>>();
Ok(map)
}
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred.");
error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
}
#[server(input = Json)]
pub async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> {
pub async fn put_provider_contact(
id: String,
contact_info: HashMap<String, ValueType>,
) -> Result<(), ServerFnError> {
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
let client = create_dynamo_client().await;
let mut item = into_attr_map(contact_info);
item.insert("subject".to_owned(), aws_sdk_dynamodb::types::AttributeValue::S(id));
item.insert(
"subject".to_owned(),
aws_sdk_dynamodb::types::AttributeValue::S(id),
);
match client
.put_item()
@@ -65,7 +75,8 @@ pub async fn put_provider_contact(id: String, contact_info: HashMap<String, Valu
Ok(_) => Ok(()),
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred.");
error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
}
}
+90 -56
View File
@@ -1,15 +1,11 @@
#[cfg(feature = "ssr")]
mod imports {
pub use leptos::logging::debug_log;
pub use aws_sdk_dynamodb::{
types::AttributeValue,
error::ProvideErrorMetadata,
};
pub use leptos::leptos_dom::log;
pub use uuid::Uuid;
pub use crate::utils::server::{create_dynamo_client};
pub use crate::common::ValueType;
pub use super::super::SCHOLARSHIPS_TABLE;
pub use crate::common::ValueType;
pub use crate::utils::server::create_dynamo_client;
pub use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue};
pub use leptos::logging::{debug_log, error, log};
pub use uuid::Uuid;
}
use crate::common::ExpandableInfo;
@@ -20,7 +16,7 @@ use leptos::server_fn::codec::Json;
#[server(GetScholarshipInfo)]
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
// Perform the operation - we just want to return all data that's contained in this entry,
@@ -39,11 +35,14 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
Ok(serde_dynamo::from_item(item)?)
} else {
log!("Couldn't find scholarship with ID {:?}", id);
Err(ServerFnError::new("Couldn't find scholarship with given ID."))
Err(ServerFnError::new(
"Couldn't find scholarship with given ID.",
))
}
}
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
@@ -51,12 +50,15 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
#[server(CreateScholarshipInfo, input = Json)]
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> {
use imports::*;
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
use imports::*;
let client = create_dynamo_client().await;
log!("Creating or updating scholarship with ID {:?}", info.subject);
log!(
"Creating or updating scholarship with ID {:?}",
info.subject
);
match client
.put_item()
@@ -68,6 +70,7 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
Ok(_) => Ok(()),
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
@@ -76,24 +79,20 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
#[server(GetAllScholarshipInfo)]
pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
debug_log!("Getting all scholarship info");
match client
.scan()
.table_name(SCHOLARSHIPS_TABLE)
.send()
.await
{
match client.scan().table_name(SCHOLARSHIPS_TABLE).send().await {
Ok(output) => {
if let Some(items) = output.items {
Ok(serde_dynamo::from_items(items)?)
} else {
Ok(Vec::new())
}
},
}
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred");
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
@@ -101,17 +100,25 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
/// Gets all scholarships that are associated with the given scholarship provider's ID.
#[server(GetProviderScholarships)]
pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<ExpandableInfo>, ServerFnError> {
pub async fn get_provider_scholarships(
provider_id: String,
) -> Result<Vec<ExpandableInfo>, ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
debug_log!("Getting provider scholarships for provider with ID {:?}", provider_id);
debug_log!(
"Getting provider scholarships for provider with ID {:?}",
provider_id
);
match client
.scan()
.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")
.send()
.await
@@ -124,7 +131,8 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
}
}
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred");
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
@@ -134,14 +142,20 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
#[server(RegisterScholarship)]
pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
debug_log!("Creating scholarship for provider with ID {:?}", provider_id);
debug_log!(
"Creating scholarship for provider with ID {:?}",
provider_id
);
let mut current_uuid = Uuid::new_v4().to_string();
let mut item = ExpandableInfo::new(current_uuid.clone());
item.data.insert("provider_id".to_string(), ValueType::String(Some(provider_id)));
item.data.insert(
"provider_id".to_string(),
ValueType::String(Some(provider_id)),
);
loop {
let ser_item = serde_dynamo::to_item(&item)?;
match client
@@ -165,10 +179,12 @@ pub async fn register_scholarship(provider_id: String) -> Result<String, ServerF
log!("Current error: {:?}", err);
current_uuid = Uuid::new_v4().to_string();
item.subject = current_uuid.clone();
continue
continue;
}
_ => {
return Err(ServerFnError::new(err.message().unwrap_or("An unknown error occurred")));
return Err(ServerFnError::new(
err.message().unwrap_or("Unknown error occurred"),
));
}
}
}
@@ -178,41 +194,51 @@ pub async fn register_scholarship(provider_id: String) -> Result<String, ServerF
/// Deletes a provider's scholarship given their provider ID and scholarship ID.
#[server(DeleteProviderScholarship)]
pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: String) -> Result<(), ServerFnError> {
pub async fn delete_provider_scholarship(
provider_id: String,
scholarship_id: String,
) -> Result<(), ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
debug_log!("Deleting scholarship with ID {:?} for provider with ID {:?}", scholarship_id, provider_id);
debug_log!(
"Deleting scholarship with ID {:?} for provider with ID {:?}",
scholarship_id,
provider_id
);
// When we delete a scholarship, we need to ensure that the provider's ID matches the scholarship,
// otherwise everyone can delete anyone else's scholarships.
match client
.delete_item()
.table_name(SCHOLARSHIPS_TABLE)
.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")
.send()
.await
{
Ok(_) => {
Ok(())
}
Ok(_) => Ok(()),
Err(err) => {
let msg = err.message().unwrap_or("An unknown error occurred");
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
Err(ServerFnError::new(msg))
}
}
}
#[server]
pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<String, crate::common::ValueType>>, ServerFnError> {
pub async fn get_all_scholarships()
-> Result<Vec<std::collections::HashMap<String, crate::common::ValueType>>, ServerFnError> {
use imports::*;
use std::collections::HashMap;
let client = create_dynamo_client().await;
client
.scan()
.table_name(SCHOLARSHIPS_TABLE)
@@ -222,12 +248,20 @@ pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<Stri
let Some(items) = output.items else {
return Vec::new();
};
items.iter().map(|item| {
// Each item should be simply converted from an AttributeValue to a ValueType.
item.iter().map(|(k, v)| (k.clone(), ValueType::from(v)))
.collect::<HashMap<String, ValueType>>()
}).collect::<Vec<_>>()
items
.iter()
.map(|item| {
// Each item should be simply converted from an AttributeValue to a ValueType.
item.iter()
.map(|(k, v)| (k.clone(), ValueType::from(v)))
.collect::<HashMap<String, ValueType>>()
})
.collect::<Vec<_>>()
})
.map_err(|err| {
let msg = err.message().unwrap_or("Unknown error occurred");
error!("{}", msg);
ServerFnError::new(msg)
})
.map_err(ServerFnError::from)
}
+54 -35
View File
@@ -3,36 +3,34 @@ use leptos::server_fn::codec::Json;
#[cfg(feature = "ssr")]
mod imports {
pub use crate::utils::server::*;
pub use aws_sdk_dynamodb::types::AttributeValue;
pub use super::super::MAIN_TABLE_NAME;
pub use crate::common::ValueType;
pub use crate::utils::server::*;
pub use aws_sdk_dynamodb::error::ProvideErrorMetadata;
pub use aws_sdk_dynamodb::types::AttributeValue;
pub use leptos::logging::{debug_log, error};
pub use std::collections::HashMap;
pub use leptos::logging::debug_log;
}
#[server(input = Json)]
pub async fn put_student_data(
subject: String,
data_type: String,
data_map: std::collections::HashMap<String, crate::common::ValueType>
data_map: std::collections::HashMap<String, crate::common::ValueType>,
) -> Result<(), ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
let mut data_map_attr = into_attr_map(data_map);
data_map_attr.insert(
"HK".into(),
AttributeValue::S(format!("STUDENT#{}", subject))
AttributeValue::S(format!("STUDENT#{}", subject)),
);
data_map_attr.insert(
"SK".into(),
AttributeValue::S(data_type.to_uppercase())
);
data_map_attr.insert("SK".into(), AttributeValue::S(data_type.to_uppercase()));
debug_log!("Inserting this item: {:?}", data_map_attr);
client
.put_item()
.table_name(MAIN_TABLE_NAME)
@@ -40,7 +38,14 @@ pub async fn put_student_data(
.send()
.await
.map(|_| ())
.map_err(ServerFnError::from)
.map_err(|err| {
let msg = format!(
"Couldn't put item into Dynamo: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})
}
#[server]
@@ -49,9 +54,9 @@ pub async fn get_student_data(
data_type: String,
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
client
.query()
.table_name(MAIN_TABLE_NAME)
@@ -65,37 +70,42 @@ pub async fn get_student_data(
let Some(list) = output.items else {
return HashMap::new();
};
let mut map = HashMap::new();
let _ = list
.into_iter()
.flatten()
.for_each(|(k, v)| {
map.insert(k, ValueType::from(&v));
});
let _ = list.into_iter().flatten().for_each(|(k, v)| {
map.insert(k, ValueType::from(&v));
});
map
})
.map_err(ServerFnError::from)
.map_err(|err| {
let msg = format!(
"Couldn't get item from Dynamo: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})
}
/// Gets and flattens all of a student's information from the database, regardless of their defined
/// sort key.
///
///
/// Note: if sorted data from the `put_student_data` function contains the same fields, this
/// function will have unexpected behavior. For example, if a `first_name` field appears in multiple
/// locations in the database, all for this single student, they will overwrite each other in no
/// specific order.
#[server]
pub async fn get_all_student_data(
subject: String
subject: String,
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
use imports::*;
let client = create_dynamo_client().await;
client.query()
client
.query()
.table_name(MAIN_TABLE_NAME)
.key_condition_expression("HK = :hk")
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject)))
@@ -105,10 +115,19 @@ pub async fn get_all_student_data(
let Some(items) = output.items else {
return HashMap::new();
};
items.iter().flatten()
.map(|(|k, v)| (k.clone(), ValueType::from(v)))
items
.iter()
.flatten()
.map(|(k, v)| (k.clone(), ValueType::from(v)))
.collect::<HashMap<String, ValueType>>()
})
.map_err(ServerFnError::from)
.map_err(|err| {
let msg = format!(
"Couldn't get data from Dynamo: {}",
err.message().unwrap_or("Unknown error occurred")
);
error!("{}", msg);
ServerFnError::new(msg)
})
}
+7 -1
View File
@@ -4,7 +4,7 @@ use leptos::prelude::*;
#[component]
pub fn StudentAcademicsPage() -> impl IntoView {
let controller = use_student_form("academics");
let controller = use_student_form("academics", true);
view! {
<Suspense fallback=Loading>
@@ -20,9 +20,11 @@ pub fn StudentAcademicsPage() -> impl IntoView {
title="Academic Information"
description="This information will allow you to be eligible for any scholarships that have specific academic/grade requirements."
on_submit=controller.submit_action
disabled=controller.submit_pending
>
<OutlinedTextField
label="Unweighted GPA:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="unweighted_gpa"
placeholder="4.0"
@@ -31,6 +33,7 @@ pub fn StudentAcademicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Weighted GPA:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="weighted_gpa"
placeholder="4.5"
@@ -39,6 +42,7 @@ pub fn StudentAcademicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Highest SAT Score:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="sat_score"
placeholder="1600"
@@ -47,6 +51,7 @@ pub fn StudentAcademicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Highest ACT Score:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="act_score"
placeholder="36"
@@ -55,6 +60,7 @@ pub fn StudentAcademicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Academic Honors:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="academic_honors"
placeholder="High Honors, etc."
+4 -2
View File
@@ -5,7 +5,7 @@ use leptos::prelude::*;
#[component]
pub fn StudentAthleticsPage() -> impl IntoView {
let controller = use_student_form("athletics");
let controller = use_student_form("athletics", true);
view! {
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
@@ -15,6 +15,7 @@ pub fn StudentAthleticsPage() -> impl IntoView {
title="Athletics Information"
description="This information will allow you to be eligible for any scholarships that have specific sports requirements."
on_submit=controller.submit_action
disabled=controller.submit_pending
>
<MultiEntry
label="Athletics Activities:"
@@ -39,7 +40,8 @@ pub fn StudentAthleticsPage() -> impl IntoView {
"Lacrosse",
"Softball",
"Tennis",
"Baseball"
"Baseball",
"Volleyball"
]
),
input!(
+12 -2
View File
@@ -9,7 +9,7 @@ pub fn StudentDemographicsPage() -> impl IntoView {
// I'm going to test out creating this form without using a panel - we'll just use the page as
// it is with a specially spaced invisible div instead of a shaded container.
let controller = use_student_form("demographics");
let controller = use_student_form("demographics", true);
view! {
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
@@ -19,9 +19,11 @@ pub fn StudentDemographicsPage() -> impl IntoView {
title="Student Demographic Form"
description="Here you'll set up some basic information about yourself."
on_submit=controller.submit_action
disabled=controller.submit_pending
>
<OutlinedTextField
label="First Name:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="first_name"
placeholder="John"
@@ -30,6 +32,7 @@ pub fn StudentDemographicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Last Name:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="last_name"
placeholder="Smith"
@@ -38,6 +41,7 @@ pub fn StudentDemographicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Date of Birth:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="dob"
placeholder="MM/DD/YYYY"
@@ -46,8 +50,9 @@ pub fn StudentDemographicsPage() -> impl IntoView {
/>
<Select
label="Gender:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="dob"
data_member="gender"
required=true
value_list=vec![
"Male".to_string(),
@@ -57,6 +62,7 @@ pub fn StudentDemographicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Student ID Number:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="id_number"
placeholder="Your 7 digit number..."
@@ -65,6 +71,7 @@ pub fn StudentDemographicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Preferred Email:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="email"
placeholder="me@example.com"
@@ -73,6 +80,7 @@ pub fn StudentDemographicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Preferred Phone Number:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="phone_number"
placeholder="000-000-0000"
@@ -81,6 +89,7 @@ pub fn StudentDemographicsPage() -> impl IntoView {
/>
<OutlinedTextField
label="Street Address:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="street_address"
placeholder="234 Judd Rd"
@@ -89,6 +98,7 @@ pub fn StudentDemographicsPage() -> impl IntoView {
/>
<Select
label="Town:"
disabled=controller.submit_pending
value_list=vec!["Southbury".to_string(), "Middlebury".to_string()]
data_map=controller.data_map
data_member="town"
+41 -28
View File
@@ -4,7 +4,7 @@ use crate::pages::api::files::list_files;
use crate::pages::api::students::get_all_student_data;
use crate::pages::api::{get_all_scholarship_info, get_comparison_info};
use crate::utils::get_user_claims;
use leptos::logging::debug_log;
use leptos::logging::{debug_error, debug_log};
use leptos::prelude::*;
use leptos_oidc::AuthSignal;
@@ -23,6 +23,11 @@ pub fn StudentEligibilityPage() -> impl IntoView {
let auth = expect_context::<AuthSignal>();
let user_claims = get_user_claims();
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
let access_token = Memo::new(move |_| auth.get().authenticated().map(|a| a.access_token()));
Effect::new(move || {
debug_log!("User ID was updated: {}", user_id.get().unwrap_or_default());
});
let form_id = StoredValue::new("scholarship_essays".to_string());
@@ -63,8 +68,7 @@ pub fn StudentEligibilityPage() -> impl IntoView {
.get("name")
.unwrap_or(&ValueType::String(None))
.as_string()
.ok()
.flatten()
.ok()?
.unwrap_or_default();
debug_log!("Checking scholarship: {}", scholarship_name);
let requirements_map = scholarship
@@ -72,8 +76,7 @@ pub fn StudentEligibilityPage() -> impl IntoView {
.get("requirements")
.unwrap_or(&ValueType::Map(None))
.as_map()
.ok()
.flatten()
.ok()?
.unwrap_or_default()
.values()
.cloned()
@@ -104,38 +107,48 @@ pub fn StudentEligibilityPage() -> impl IntoView {
debug_log!(
"Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>()
);
let valid = resolved_requirements
.iter()
.all(|list| {
list.iter()
.fold(
false,
|prev, requirement| {
let result = requirement
.compare(&student_info)
.unwrap_or(false);
if prev { prev } else { result }
},
)
});
let valid = !scholarship_name.is_empty()
&& resolved_requirements
.iter()
.all(|list| {
debug_log!("Checking list: {:?}", list);
if list.is_empty() {
true
} else {
list.iter()
.any(|requirement| {
let result = requirement.compare(&student_info);
match result.as_ref() {
Ok(status) => {
debug_log!(
"Requirement with id {} is Ok({})", requirement.id, status
);
status.clone()
}
Err(e) => {
debug_error!(
"Requirement with id {} failed: {}", requirement.id, e
);
false
}
}
})
}
});
if valid {
debug_log!("Scholarship is valid.");
let scholarship_id = StoredValue::new(scholarship.subject.clone());
let resource = Resource::new(
move || (
auth
.try_with(|a| a.authenticated().map(|a| a.access_token()))
.flatten(),
access_token.get(),
form_id.get_value(),
scholarship_id.get_value(),
),
async move |(access_token, form_id, scholarship_id)| {
list_files(
access_token.unwrap_or_default(),
form_id,
scholarship_id,
)
.await
let Some(access_token) = access_token else {
return Err(ServerFnError::new("Couldn't find access token"));
};
list_files(access_token, form_id, scholarship_id).await
},
);
Some((scholarship.clone(), resource))
+4 -2
View File
@@ -5,20 +5,22 @@ use crate::pages::student::form_setup::use_student_form;
#[component]
pub fn StudentExtracurricularsPage() -> impl IntoView {
let controller = use_student_form("demographics");
let controller = use_student_form("demographics", true);
view! {
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
<div class="flex flex-1" />
<div class="flex flex-col flex-2 mt-6">
<ValidatedForm
title="Extracurricular Information"
title="Extracurricular and Community Service Information"
description="This information will allow you to be eligible for any scholarships that have specific extracurricular requirements."
on_submit=controller.submit_action
disabled=controller.submit_pending
>
// Inputs here.
<OutlinedTextField
label="Total number of service hours:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="service_hours"
placeholder="Any number..."
+12 -1
View File
@@ -4,7 +4,7 @@ use crate::pages::student::form_setup::use_student_form;
#[component]
pub fn StudentFamilyPage() -> impl IntoView {
let controller = use_student_form("family");
let controller = use_student_form("family", true);
view! {
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
@@ -14,10 +14,12 @@ pub fn StudentFamilyPage() -> impl IntoView {
title="Family Information"
description="General family information. You must fill out information about at least one parent."
on_submit=controller.submit_action
disabled=controller.submit_pending
>
// Inputs here.
<OutlinedTextField
label="Total number of children in family:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="num_children"
placeholder="Any number..."
@@ -26,6 +28,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
/>
<OutlinedTextField
label="Total number of children currently attending college:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="num_children_college"
placeholder="Any number..."
@@ -34,6 +37,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
/>
<OutlinedTextField
label="Parent/Guardian 1 Name:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="parent_one_name"
placeholder="John Smith"
@@ -43,6 +47,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
// TODO Could this be a select dropdown?
<OutlinedTextField
label="Parent/Guardian 1 Relationship:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="parent_one_relationship"
placeholder="Mother/Father"
@@ -52,6 +57,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
// TODO Get a better example
<OutlinedTextField
label="Parent/Guardian 1 Occupation:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="parent_one_occupation"
placeholder="Milkman"
@@ -60,6 +66,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
/>
<OutlinedTextField
label="Parent/Guardian 1 Employer:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="parent_one_employer"
placeholder="Example Employer"
@@ -69,6 +76,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
<OutlinedTextField
label="Parent/Guardian 2 Name:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="parent_two_name"
placeholder="John Smith"
@@ -77,6 +85,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
/>
<OutlinedTextField
label="Parent/Guardian 2 Relationship:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="parent_two_relationship"
placeholder="Mother/Father"
@@ -85,6 +94,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
/>
<OutlinedTextField
label="Parent/Guardian 2 Occupation:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="parent_two_occupation"
placeholder="Milkman"
@@ -93,6 +103,7 @@ pub fn StudentFamilyPage() -> impl IntoView {
/>
<OutlinedTextField
label="Parent/Guardian 2 Employer:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="parent_two_employer"
placeholder="Example Employer"
+55 -22
View File
@@ -1,37 +1,40 @@
use std::collections::HashMap;
use leptos::prelude::*;
use crate::common::ValueType;
use crate::pages::api::students::{get_student_data, PutStudentData};
use crate::components::{Toast, ToastContext};
use crate::pages::api::students::{PutStudentData, get_student_data};
use crate::utils::get_user_claims;
use leptos::prelude::*;
use std::collections::HashMap;
use uuid::Uuid;
pub struct StudentFormInfo {
pub data_map: RwSignal<HashMap<String, ValueType>>,
pub user_id: Memo<Option<String>>,
pub submit_action: Callback<()>,
pub refresh_trigger: Trigger,
pub data_resource: Resource<Result<HashMap<String, ValueType>, ServerFnError>>
pub data_resource: Resource<Result<HashMap<String, ValueType>, ServerFnError>>,
pub submit_pending: Memo<bool>,
}
pub fn use_student_form(
form_type: impl Into<Signal<String>>
form_type: impl Into<Signal<String>>,
enable_toasts: bool,
) -> StudentFormInfo {
let form_type = form_type.into();
let user_claims = get_user_claims();
let user_id = Memo::new(move |_| {
user_claims.get()
.map(|info| info.claims.subject.clone())
});
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
let data_map = RwSignal::new(HashMap::new());
let refresh_trigger = Trigger::new();
let data_resource = Resource::new(
move || (user_id.get().unwrap_or_default(), form_type.get(), refresh_trigger.track()),
async move |(id, form_type, _)| get_student_data(
id,
form_type,
).await
move || (user_id.get(), form_type.get(), refresh_trigger.track()),
async move |(id, form_type, _)| {
let Some(id) = id else {
return Ok(HashMap::new());
};
get_student_data(id, form_type).await
},
);
Effect::new(move || {
@@ -46,18 +49,48 @@ pub fn use_student_form(
let submit_action = ServerAction::<PutStudentData>::new();
let on_submit = move || {
submit_action.dispatch(PutStudentData {
subject: user_id.get().unwrap_or_default(),
data_type: form_type.get(),
data_map: data_map.get()
});
if let Some(id) = user_id.get() {
submit_action.dispatch(PutStudentData {
subject: id,
data_type: form_type.get(),
data_map: data_map.get(),
});
}
};
if enable_toasts {
let mut toasts = expect_context::<ToastContext>();
Effect::watch(
move || submit_action.value().get(),
move |value, _, _| {
let Some(result) = value else {
return;
};
let toast = match result {
Ok(_) => Toast::new()
.id(Uuid::new_v4())
.header("Submission Successful")
.msg("You may continue editing or fill out another form."),
Err(e) => Toast::new()
.id(Uuid::new_v4())
.header("Submission Failed")
.msg(e.to_string()),
};
toasts.toast(toast);
untrack(move || submit_action.clear());
},
false,
);
}
StudentFormInfo {
data_map,
refresh_trigger,
data_resource,
user_id,
submit_action: on_submit.into(),
submit_pending: submit_action.pending(),
}
}
}
+1
View File
@@ -13,6 +13,7 @@ pub fn StudentHomePage() -> impl IntoView {
"ssmith@region15.org"
</a>"."
</div>
<div class="text-lg">"The due date for this form is on April 2nd, 2026 at 2:05pm."</div>
<div class="text-lg">"Here are some links to get you started:"</div>
<DashboardButton
title="Scholarship Book"
@@ -4,7 +4,7 @@ use leptos::prelude::*;
#[component]
pub fn StudentSpecificRequirementsPage() -> impl IntoView {
let controller = use_student_form("specifics");
let controller = use_student_form("specifics", true);
view! {
<Suspense fallback=Loading>
@@ -20,9 +20,11 @@ pub fn StudentSpecificRequirementsPage() -> impl IntoView {
title="Scholarship-Specific Eligibility Requirements"
description="These questions are specific to some scholarships. Indicate yes or no for each question."
on_submit=controller.submit_action
disabled=controller.submit_pending
>
<RadioList
label="Have you attended BAS?"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="attend_bas"
items=vec!["Yes".to_string(), "No".to_string()]
@@ -30,6 +32,7 @@ pub fn StudentSpecificRequirementsPage() -> impl IntoView {
/>
<RadioList
label="Are you a member of Midd-South Catholic Church?"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="middsouth_church"
items=vec!["Yes".to_string(), "No".to_string()]
@@ -37,6 +40,7 @@ pub fn StudentSpecificRequirementsPage() -> impl IntoView {
/>
<RadioList
label="Do you have a family member that is or has served in the US military? (Note: you may be asked for proof of service)"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="family_military_service"
items=vec!["Yes".to_string(), "No".to_string()]
@@ -44,6 +48,7 @@ pub fn StudentSpecificRequirementsPage() -> impl IntoView {
/>
<RadioList
label="Have you participated in Pomperaug Youth Baseball?"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="youth_baseball"
items=vec!["Yes".to_string(), "No".to_string()]
@@ -51,6 +56,7 @@ pub fn StudentSpecificRequirementsPage() -> impl IntoView {
/>
<RadioList
label="Have you participated in the Panthers Aquatic Club?"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="aquatic_club"
items=vec!["Yes".to_string(), "No".to_string()]
@@ -58,6 +64,7 @@ pub fn StudentSpecificRequirementsPage() -> impl IntoView {
/>
<RadioList
label="Are at least one of your parents a member of the Region 15 PEA?"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="pea_member"
items=vec!["Yes".to_string(), "No".to_string()]
@@ -65,6 +72,7 @@ pub fn StudentSpecificRequirementsPage() -> impl IntoView {
/>
<RadioList
label="Have you participated in the PHS Music Program (Band, Chorus, etc)?"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="music_program"
items=vec!["Yes".to_string(), "No".to_string()]
+11 -2
View File
@@ -4,7 +4,7 @@ use crate::pages::student::form_setup::use_student_form;
#[component]
pub fn StudentUniversityPage() -> impl IntoView {
let controller = use_student_form("university");
let controller = use_student_form("university", true);
// college name, state, city, zip
// major, study field, intended career, college_acceptance
@@ -17,10 +17,12 @@ pub fn StudentUniversityPage() -> impl IntoView {
title="University Information"
description="Here you may fill out general information about the university you plan to attend."
on_submit=controller.submit_action
disabled=controller.submit_pending
>
// Inputs here.
<OutlinedTextField
label="University Name:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="college_name"
placeholder="University of Example"
@@ -29,6 +31,7 @@ pub fn StudentUniversityPage() -> impl IntoView {
/>
<OutlinedTextField
label="University Street Address:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="college_city"
placeholder="123 Example Rd"
@@ -37,6 +40,7 @@ pub fn StudentUniversityPage() -> impl IntoView {
/>
<OutlinedTextField
label="University State:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="college_state"
placeholder="CT, AZ, etc."
@@ -45,6 +49,7 @@ pub fn StudentUniversityPage() -> impl IntoView {
/>
<OutlinedTextField
label="University ZIP:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="college_zip"
placeholder="12345"
@@ -53,13 +58,15 @@ pub fn StudentUniversityPage() -> impl IntoView {
/>
<Select
label="Have you been sent an acceptance to the university?"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="college_acceptance"
required=true
value_list=vec!["Yes".to_string(), "No".to_string()]
/>
<Select
label="Chosen Major:"
label="Intended Major/Field of Study:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="major"
required=true
@@ -90,10 +97,12 @@ pub fn StudentUniversityPage() -> impl IntoView {
"Landscaping".to_string(),
"Legal Studies".to_string(),
"Criminal Justice".to_string(),
"None of the above".to_string(),
]
/>
<OutlinedTextField
label="Intended Career:"
disabled=controller.submit_pending
data_map=controller.data_map
data_member="intended_career"
placeholder="Engineer, Artist, etc."
+2 -1
View File
@@ -5,7 +5,7 @@ use leptos::prelude::*;
#[component]
pub fn StudentWorkExperiencePage() -> impl IntoView {
let controller = use_student_form("workexp");
let controller = use_student_form("workexp", true);
view! {
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
@@ -15,6 +15,7 @@ pub fn StudentWorkExperiencePage() -> impl IntoView {
title="Student Work Experience"
description="Here you may add information about your past/current workplace."
on_submit=controller.submit_action
disabled=controller.submit_pending
>
<MultiEntry
label="Work Experience:"