Merge branch 'main' into feat/date-component
# Conflicts: # src/app.rs # src/common/mod.rs # src/components/mod.rs # src/pages/api/scholarships.rs # src/pages/home_page.rs # src/pages/mod.rs # src/pages/scholarship_info.rs
This commit is contained in:
commit
26ee1cf7b4
50 files changed
+3428
-2237
No files matched your search
+151
-30
@@ -1,19 +1,25 @@
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::pages::utils::server_utils::create_dynamo_client;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::logging::log;
|
||||
|
||||
use crate::common::{
|
||||
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
|
||||
TextComparison, TextListComparison, ValueType,
|
||||
};
|
||||
use leptos::leptos_dom::log;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use leptos::server;
|
||||
|
||||
static COMPARISONS_TABLE: &str = "leptos-comparison-test";
|
||||
|
||||
/// 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.
|
||||
#[allow(unused)]
|
||||
fn create_sports_comparisons() -> Vec<ComparisonData> {
|
||||
vec![
|
||||
ComparisonData::new(
|
||||
@@ -184,11 +190,74 @@ fn create_sports_comparisons() -> Vec<ComparisonData> {
|
||||
]
|
||||
}
|
||||
|
||||
#[server(CreateTestComparisons, endpoint = "/comparisons/create-test")]
|
||||
pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
let client = create_dynamo_client().await;
|
||||
fn create_major_comparison(
|
||||
id: impl Into<String>,
|
||||
display_text: impl Into<String>,
|
||||
) -> ComparisonData {
|
||||
let display = display_text.into();
|
||||
ComparisonData::new(
|
||||
id,
|
||||
"major",
|
||||
ComparisonType::Text(TextComparison::Contains),
|
||||
ValueType::String(Some(display.clone())),
|
||||
"Majors",
|
||||
display,
|
||||
)
|
||||
}
|
||||
|
||||
log!("Creating test comparisons");
|
||||
fn create_major_comparisons() -> Vec<ComparisonData> {
|
||||
let display_texts = vec![
|
||||
"Music",
|
||||
"Education",
|
||||
"Special Education",
|
||||
"Speech Pathology",
|
||||
"School Psychology",
|
||||
"School Counseling",
|
||||
"Occupational Therapy",
|
||||
"Physical Therapy",
|
||||
"Nursing",
|
||||
"Allied Health",
|
||||
"Fine/Performing Arts",
|
||||
"Writing/Communication",
|
||||
"History",
|
||||
"Government",
|
||||
"Political Science",
|
||||
"Social Work",
|
||||
"Sports Medicine",
|
||||
"Athletic Training",
|
||||
"Horticulture",
|
||||
"Conservation Studies",
|
||||
"Ecology",
|
||||
"Environmental Studies",
|
||||
"Urban Planning",
|
||||
"Landscaping",
|
||||
"Legal Studies",
|
||||
"Criminal Justice",
|
||||
];
|
||||
|
||||
let ids = display_texts
|
||||
.iter()
|
||||
.map(|text|
|
||||
format!("{}_{}", "major", text.to_lowercase().replace(" ", "_").replace("/", "_"))
|
||||
)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
ids.iter().zip(display_texts).map(|(id, display_text)| {
|
||||
create_major_comparison(id, display_text)
|
||||
})
|
||||
.collect::<Vec<ComparisonData>>()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn make_comp_list() -> Vec<ComparisonData> {
|
||||
let gpa_3 = ComparisonData::new(
|
||||
"gpa_3",
|
||||
"weighted_gpa",
|
||||
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
|
||||
ValueType::Number(Some(3.0.to_string())),
|
||||
"GPA Limits",
|
||||
"GPA >= 3.0",
|
||||
);
|
||||
|
||||
let test_comp = ComparisonData::new(
|
||||
"math_sat_comp",
|
||||
@@ -198,21 +267,7 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
"Academic Information",
|
||||
"Math SAT Score > 650",
|
||||
);
|
||||
|
||||
let service_hours_25 = ComparisonData::new(
|
||||
"map_sum_comp",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
ValueType::Number(Some(25.to_string())),
|
||||
"Community Involvement",
|
||||
"25 service hours",
|
||||
);
|
||||
|
||||
|
||||
let service_hours_20 = ComparisonData::new(
|
||||
"service_hours_20",
|
||||
"community_involvement",
|
||||
@@ -223,8 +278,22 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
))),
|
||||
)),
|
||||
ValueType::Number(Some(20.to_string())),
|
||||
"Community Involvement",
|
||||
"20 service hours",
|
||||
"Community Service",
|
||||
"20+ service hours",
|
||||
);
|
||||
|
||||
let service_hours_25 = ComparisonData::new(
|
||||
"service_hours_25",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
ValueType::Number(Some(25.to_string())),
|
||||
"Community Service",
|
||||
"25+ service hours",
|
||||
);
|
||||
|
||||
let service_hours_30 = ComparisonData::new(
|
||||
@@ -237,8 +306,8 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
))),
|
||||
)),
|
||||
ValueType::Number(Some(30.to_string())),
|
||||
"Community Involvement",
|
||||
"30 service hours",
|
||||
"Community Service",
|
||||
"30+ service hours",
|
||||
);
|
||||
|
||||
let residency_southbury = ComparisonData::new(
|
||||
@@ -264,7 +333,7 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
"attended_bas",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Miscellaneous",
|
||||
"Additional Eligibility Factors",
|
||||
"Attended BAS",
|
||||
);
|
||||
|
||||
@@ -273,7 +342,7 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
"middsouth_church",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Miscellaneous",
|
||||
"Additional Eligibility Factors",
|
||||
"Member of Midd-South Church",
|
||||
);
|
||||
|
||||
@@ -282,14 +351,15 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
"family_military_service",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Miscellaneous",
|
||||
"Additional Eligibility Factors",
|
||||
"Family with Military Service",
|
||||
);
|
||||
|
||||
let mut sports_comps = create_sports_comparisons();
|
||||
let mut major_comps = Vec::<ComparisonData>::new();
|
||||
let mut major_comps = create_major_comparisons();
|
||||
|
||||
let mut comp_list = vec![
|
||||
gpa_3,
|
||||
test_comp,
|
||||
service_hours_20,
|
||||
service_hours_25,
|
||||
@@ -304,10 +374,21 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
comp_list.append(&mut sports_comps);
|
||||
comp_list.append(&mut major_comps);
|
||||
|
||||
comp_list
|
||||
}
|
||||
|
||||
#[server(CreateTestComparisons, endpoint = "/comparisons/create-test")]
|
||||
pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Creating test comparisons");
|
||||
|
||||
let comp_list = make_comp_list();
|
||||
|
||||
for comparison in comp_list {
|
||||
if let Err(err) = client
|
||||
.put_item()
|
||||
.table_name("leptos-comparison-test")
|
||||
.table_name(COMPARISONS_TABLE)
|
||||
.set_item(Some(serde_dynamo::to_item(&comparison)?))
|
||||
.send()
|
||||
.await
|
||||
@@ -330,7 +411,7 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
|
||||
// id and display text, but we'll return the whole thing.
|
||||
match client
|
||||
.scan()
|
||||
.table_name("leptos-comparison-test")
|
||||
.table_name(COMPARISONS_TABLE)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
@@ -348,3 +429,43 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
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
|
||||
{
|
||||
Ok(output) => {
|
||||
if let Some(items) = output.items {
|
||||
log!("Found comparisons, categorizing.");
|
||||
|
||||
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.
|
||||
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."))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||
Err(ServerFnError::new(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue};
|
||||
|
||||
use crate::common::{ExpandableInfo, ValueType};
|
||||
use aws_sdk_dynamodb::{
|
||||
types::AttributeValue,
|
||||
error::ProvideErrorMetadata,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::leptos_dom::log;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use leptos::server;
|
||||
#[cfg(feature = "ssr")]
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::pages::utils::server_utils::create_dynamo_client;
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::common::ValueType;
|
||||
|
||||
use crate::common::ExpandableInfo;
|
||||
|
||||
use leptos::prelude::ServerFnError;
|
||||
use leptos::server;
|
||||
|
||||
static SCHOLARSHIPS_TABLE: &str = "leptos-scholarship-test";
|
||||
|
||||
#[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")]
|
||||
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
|
||||
@@ -19,7 +28,7 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
|
||||
log!("Getting scholarship info using id {:?}", id);
|
||||
match client
|
||||
.get_item()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.key("subject", AttributeValue::S(id.clone()))
|
||||
.send()
|
||||
.await
|
||||
@@ -30,9 +39,7 @@ 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) => {
|
||||
@@ -49,14 +56,11 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
|
||||
|
||||
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()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.set_item(Some(serde_dynamo::to_item(&info)?))
|
||||
.send()
|
||||
.await
|
||||
@@ -75,7 +79,7 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
|
||||
log!("Getting all scholarship info");
|
||||
match client
|
||||
.scan()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
@@ -85,7 +89,7 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||
Err(ServerFnError::new(msg))
|
||||
@@ -95,23 +99,15 @@ 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, "/providers/scholarships/get")]
|
||||
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> {
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!(
|
||||
"Getting provider scholarships for provider with ID {:?}",
|
||||
provider_id
|
||||
);
|
||||
|
||||
|
||||
log!("Getting provider scholarships for provider with ID {:?}", provider_id);
|
||||
|
||||
match client
|
||||
.scan()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.expression_attribute_values(
|
||||
":id",
|
||||
serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?,
|
||||
)
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.expression_attribute_values(":id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?)
|
||||
.filter_expression("provider_id = :id")
|
||||
.send()
|
||||
.await
|
||||
@@ -132,32 +128,20 @@ pub async fn get_provider_scholarships(
|
||||
|
||||
/// Creates a new scholarship with a unique ID, and then returns that ID.
|
||||
#[server(RegisterScholarship, "/providers/scholarships/create")]
|
||||
pub async fn register_scholarship(
|
||||
provider_id: String,
|
||||
scholarship_name: String,
|
||||
) -> Result<String, ServerFnError> {
|
||||
pub async fn register_scholarship(provider_id: String, scholarship_name: String) -> Result<String, ServerFnError> {
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!(
|
||||
"Creating scholarship for provider with ID {:?}",
|
||||
provider_id
|
||||
);
|
||||
|
||||
|
||||
log!("Creating scholarship for provider with ID {:?}", provider_id);
|
||||
|
||||
let mut current_uuid = "testing".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(
|
||||
"scholarship_name".to_string(),
|
||||
ValueType::String(Some(scholarship_name)),
|
||||
);
|
||||
item.data.insert("provider_id".to_string(), ValueType::String(Some(provider_id)));
|
||||
item.data.insert("scholarship_name".to_string(), ValueType::String(Some(scholarship_name)));
|
||||
loop {
|
||||
let ser_item = serde_dynamo::to_item(&item)?;
|
||||
match client
|
||||
.put_item()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.set_item(Some(ser_item))
|
||||
.condition_expression("attribute_not_exists(subject)")
|
||||
.send()
|
||||
@@ -176,12 +160,10 @@ pub async fn register_scholarship(
|
||||
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("An unknown error occurred")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,33 +173,25 @@ pub async fn register_scholarship(
|
||||
|
||||
/// Deletes a provider's scholarship given their provider ID and scholarship ID.
|
||||
#[server(DeleteProviderScholarship, "/providers/scholarships/delete")]
|
||||
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> {
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!(
|
||||
"Deleting scholarship with ID {:?} for provider with ID {:?}",
|
||||
scholarship_id,
|
||||
provider_id
|
||||
);
|
||||
|
||||
|
||||
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("leptos-scholarship-test")
|
||||
.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");
|
||||
Err(ServerFnError::new(msg))
|
||||
|
||||
+232
-205
@@ -1,30 +1,31 @@
|
||||
// Server dependencies
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use serde_dynamo::{from_item, to_item};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::io::Write;
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::process::{Command, Stdio};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::io::Write;
|
||||
|
||||
use crate::common::{ExpandableInfo, UserClaims};
|
||||
use crate::components::{
|
||||
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
|
||||
Select,
|
||||
};
|
||||
use crate::input;
|
||||
use crate::common::{ComparisonData, ExpandableInfo};
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
use crate::pages::utils::get_user_claims;
|
||||
use base64::Engine;
|
||||
use crate::components::{
|
||||
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
|
||||
Select, TextFieldType, ValidatedForm
|
||||
};
|
||||
use crate::input;
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos::task::spawn_local;
|
||||
use leptos::wasm_bindgen::JsCast;
|
||||
use leptos::web_sys::HtmlAnchorElement;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use traits::{AsReactive, ReactiveCapture};
|
||||
use base64::Engine;
|
||||
use leptos::logging::log;
|
||||
use leptos::task::spawn_local;
|
||||
use leptos::web_sys::HtmlAnchorElement;
|
||||
use leptos::wasm_bindgen::JsCast;
|
||||
use std::collections::HashMap;
|
||||
use crate::pages::api::get_comparisons_categorized;
|
||||
|
||||
/// # Get Student Info
|
||||
/// Gets a student's information given their `subject`.
|
||||
@@ -144,24 +145,96 @@ pub async fn create_example_pdf() -> Result<Vec<u8>, ServerFnError> {
|
||||
/// The main home page component. Contains a simple contact form.
|
||||
#[component]
|
||||
pub fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
//#region Server Resources
|
||||
let user_claims = get_user_claims();
|
||||
let user_subject = Memo::new(move |_| {
|
||||
user_claims.get()
|
||||
.map(|claim| claim.claims.subject.clone())
|
||||
});
|
||||
|
||||
// Note that the value passed in MUST be equatable.
|
||||
// We get/unwrap the value repeatedly until we get a simple string value, then clone it so that
|
||||
// we don't lose access to it in the future, should we need it again.
|
||||
let server_resource: Resource<ExpandableInfo> = Resource::new(
|
||||
move || user_claims.get().map(|claim| claim.claims.subject.clone()),
|
||||
async |opt_username| match opt_username {
|
||||
Some(subject) => get_submission(subject.clone()).await.unwrap_or_else(|e| {
|
||||
console_log(e.to_string().as_str());
|
||||
ExpandableInfo::new(subject)
|
||||
}),
|
||||
None => ExpandableInfo::new("".to_owned()),
|
||||
},
|
||||
let form_data = RwSignal::new(HashMap::new());
|
||||
|
||||
let server_resource = Resource::new(
|
||||
move || user_subject.get(),
|
||||
async move |subject| get_submission(subject.unwrap_or_default()).await
|
||||
);
|
||||
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
||||
|
||||
Effect::new(move || {
|
||||
match server_resource.get() {
|
||||
Some(Ok(submission)) => {
|
||||
form_data.set(submission.data);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!("Error while getting information: {:?}", err);
|
||||
}
|
||||
_ => {
|
||||
log!("An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
//#region Component State
|
||||
let elements_disabled = Signal::derive(move || {
|
||||
submit_action.pending().get()
|
||||
});
|
||||
//#endregion
|
||||
//#region Event logic
|
||||
let on_submit = move |_| {
|
||||
let mut exp_info = ExpandableInfo::new(user_subject.get().unwrap_or_default());
|
||||
exp_info.data = form_data.get();
|
||||
submit_action.dispatch(CreateSampleSubmission {
|
||||
student_info: exp_info
|
||||
});
|
||||
};
|
||||
|
||||
let pdf_button_click = move |_| {
|
||||
console_log("Attempting to get PDF from server endpoint");
|
||||
spawn_local(async move {
|
||||
let result = create_example_pdf().await;
|
||||
if let Ok(bytes) = result {
|
||||
let base64 = base64::engine::general_purpose::STANDARD
|
||||
.encode(bytes);
|
||||
let data_url = format!(
|
||||
"data:application/pdf;base64,{}",
|
||||
base64,
|
||||
);
|
||||
console_log("Found document, opening in new tab...");
|
||||
let link = document()
|
||||
.create_element("a")
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlAnchorElement>()
|
||||
.unwrap();
|
||||
link.set_href(&*data_url);
|
||||
link.set_target("_blank");
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
};
|
||||
//#endregion
|
||||
|
||||
let comparison_resource = Resource::new(
|
||||
move || user_subject.get(),
|
||||
async move |_| get_comparisons_categorized().await
|
||||
);
|
||||
|
||||
let comparison_info = RwSignal::new(HashMap::<String, Vec<ComparisonData>>::new());
|
||||
|
||||
Effect::new(move || {
|
||||
match comparison_resource.get() {
|
||||
Some(Ok(comparisons)) => {
|
||||
log!("Found comparison info: {:?}", comparisons);
|
||||
comparison_info.set(comparisons);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!("Error while getting comparisons: {:?}", err);
|
||||
}
|
||||
_ => {
|
||||
log!("An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
@@ -170,195 +243,149 @@ pub fn HomePage() -> impl IntoView {
|
||||
{move || {
|
||||
server_resource
|
||||
.get()
|
||||
.map(|submission| {
|
||||
let expandable_react = submission.as_reactive();
|
||||
let elements_disabled = RwSignal::new(false);
|
||||
let result_msg = Signal::derive(move || {
|
||||
if submit_action.pending().get() {
|
||||
elements_disabled.set(true);
|
||||
"Sending request...".to_owned()
|
||||
} else {
|
||||
elements_disabled.set(false);
|
||||
if let Some(result) = submit_action.value().get() {
|
||||
match result {
|
||||
Ok(_) => "Request sent successfully.".to_owned(),
|
||||
Err(e) => format!("Failed to send request: {:?}", e),
|
||||
}
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
});
|
||||
.map(|_| {
|
||||
view! {
|
||||
<Row>
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
"Region 15 Scholarship Application (DEMO)"
|
||||
</h1>
|
||||
</Row>
|
||||
<Row>
|
||||
<p class="text-sm">
|
||||
"This is a very simple demonstration of the scholarship application written in Rust."
|
||||
</p>
|
||||
</Row>
|
||||
<Row>
|
||||
// Text { data_member, label, placeholder }
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member="first_name"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member="last_name"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Phone Number:"
|
||||
placeholder="123-456-7890"
|
||||
disabled=elements_disabled
|
||||
data_member="phone_number"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Street Address:"
|
||||
placeholder="123 Fake Street"
|
||||
disabled=elements_disabled
|
||||
data_member="address"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Highest Math SAT Score:"
|
||||
placeholder="600"
|
||||
disabled=elements_disabled
|
||||
data_member="math_sat"
|
||||
data_map=expandable_react.data
|
||||
input_type="number"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Radio { data_member, label, items }
|
||||
<RadioList
|
||||
label="Town:"
|
||||
items=vec!["Southbury", "Middlebury"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="town"
|
||||
data_map = expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Checkbox { data_member, label, items }
|
||||
<CheckboxList
|
||||
label="Favorite Candies:"
|
||||
items=vec!["Twizzlers", "Reese's", "Starburst"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="favorite_candies"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Select { data_member, label, items }
|
||||
<Select
|
||||
label="Gender:"
|
||||
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="gender"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<MultiEntry
|
||||
data_map=expandable_react.data
|
||||
data_member="community_involvement"
|
||||
schema=vec![
|
||||
input!(Text, "service_name", "Activity Name:", "Some service activity..."),
|
||||
input!(Number, "service_hours", "Total Service Hours", "20")
|
||||
]
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
let captured_map = expandable_react.capture();
|
||||
console_log(
|
||||
format!("Map values: {:?}", captured_map).as_str(),
|
||||
);
|
||||
submit_action
|
||||
.dispatch(CreateSampleSubmission {
|
||||
student_info: captured_map,
|
||||
});
|
||||
}
|
||||
disabled=elements_disabled
|
||||
>
|
||||
"Submit"
|
||||
</ActionButton>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton on:click=move |_| {
|
||||
console_log("Attempting to get PDF from server endpoint");
|
||||
spawn_local(async move {
|
||||
let result = create_example_pdf().await;
|
||||
if let Ok(bytes) = result {
|
||||
let base64 = base64::engine::general_purpose::STANDARD
|
||||
.encode(bytes);
|
||||
let data_url = format!(
|
||||
"data:application/pdf;base64,{}",
|
||||
base64,
|
||||
);
|
||||
console_log("Found document, opening in new tab...");
|
||||
let link = document()
|
||||
.create_element("a")
|
||||
.unwrap()
|
||||
.dyn_into::<HtmlAnchorElement>()
|
||||
.unwrap();
|
||||
link.set_href(&*data_url);
|
||||
link.set_target("_blank");
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
}>"Get PDF"</ActionButton>
|
||||
</Row>
|
||||
<Row>
|
||||
<p>{result_msg}</p>
|
||||
</Row>
|
||||
<ValidatedForm
|
||||
on_submit=Callback::new(on_submit)
|
||||
title="Home Page Test Form"
|
||||
description="This really is just a test."
|
||||
>
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
"Region 15 Scholarship Application (DEMO)"
|
||||
</h1>
|
||||
</Row>
|
||||
<Row>
|
||||
<p class="text-sm">
|
||||
"This is a very simple demonstration of the scholarship application written in Rust."
|
||||
</p>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member="first_name"
|
||||
data_map=form_data
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member="last_name"
|
||||
data_map=form_data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=form_data
|
||||
input_type=TextFieldType::Email(
|
||||
vec!["fakeemail.org".to_string(), "gmail.com".to_string()]
|
||||
)
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Phone Number:"
|
||||
placeholder="123-456-7890"
|
||||
disabled=elements_disabled
|
||||
data_member="phone_number"
|
||||
data_map=form_data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Street Address:"
|
||||
placeholder="123 Fake Street"
|
||||
disabled=elements_disabled
|
||||
data_member="address"
|
||||
data_map=form_data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Highest Math SAT Score:"
|
||||
placeholder="600"
|
||||
disabled=elements_disabled
|
||||
data_member="math_sat"
|
||||
data_map=form_data
|
||||
input_type=TextFieldType::Number
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
label="Town:"
|
||||
items=vec!["Southbury", "Middlebury"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="town"
|
||||
data_map=form_data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<CheckboxList
|
||||
label="Favorite Candies:"
|
||||
items=vec!["Twizzlers", "Reese's", "Starburst"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="favorite_candies"
|
||||
data_map=form_data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Select
|
||||
label="Gender:"
|
||||
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="gender"
|
||||
data_map=form_data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<MultiEntry
|
||||
data_map=form_data
|
||||
data_member="community_involvement"
|
||||
schema=vec![
|
||||
input!(Text, "service_name", "Activity Name:", true, "Some service activity..."),
|
||||
input!(Number, "service_hours", "Total Service Hours", true, "20")
|
||||
]
|
||||
/>
|
||||
</Row>
|
||||
</ValidatedForm>
|
||||
</Panel>
|
||||
<div class="flex flex-col flex-1" />
|
||||
</Row>
|
||||
}
|
||||
})
|
||||
}}
|
||||
<Row>
|
||||
<ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton>
|
||||
</Row>
|
||||
</Suspense>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
+149
-96
@@ -4,10 +4,11 @@ use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValu
|
||||
#[cfg(feature = "ssr")]
|
||||
use serde_dynamo::{from_item, to_item};
|
||||
|
||||
use crate::common::{ExpandableInfo, ValueType};
|
||||
use crate::components::{
|
||||
ActionButton, Banner, DashboardButton, OutlinedTextField, Panel, Row, Select,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::common::ValueType;
|
||||
|
||||
use crate::common::ExpandableInfo;
|
||||
use crate::components::{ActionButton, Banner, DashboardButton, OutlinedTextField, Panel, Row, Select, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use chrono::{FixedOffset, TimeZone};
|
||||
use leptos::Params;
|
||||
use leptos::either::{Either, EitherOf3};
|
||||
@@ -18,7 +19,13 @@ use leptos_router::hooks::{use_navigate, use_params};
|
||||
use leptos_router::params::Params;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use traits::{AsReactive, ReactiveCapture};
|
||||
use leptos_router::components::Outlet;
|
||||
use leptos_animate::animate;
|
||||
use crate::common::animations::pop_in_out;
|
||||
// Loaner page creates a single page that contains the borrowing/returning buttons
|
||||
// and a panel for a form. At first, it will show a fallback view. At the borrowing
|
||||
// path it will show the borrow form, and at the returning path it will show the
|
||||
// return form.
|
||||
|
||||
#[derive(Params, PartialEq)]
|
||||
struct LoanerParams {
|
||||
@@ -191,7 +198,7 @@ async fn create_borrow_entry(input: ExpandableInfo) -> Result<(), ServerFnError>
|
||||
/// create a true admin page for this, simply to be able to survey and override/remove information
|
||||
/// from the lists.
|
||||
#[component]
|
||||
pub fn LoanerPage() -> impl IntoView {
|
||||
pub fn LoanerShell() -> impl IntoView {
|
||||
// Check URL params. Depending on what string is visible here, we'll display a different form.
|
||||
let params = use_params::<LoanerParams>();
|
||||
let form_passed = move || {
|
||||
@@ -203,12 +210,6 @@ pub fn LoanerPage() -> impl IntoView {
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let form_view = move || match form_passed().as_str() {
|
||||
"borrowing" => EitherOf3::A(LoanerBorrowForm),
|
||||
"returning" => EitherOf3::B(LoanerReturnForm),
|
||||
_ => EitherOf3::C("Choose something from the left.".into_view()),
|
||||
};
|
||||
|
||||
// We'll create the page here - the sidebar contains all the buttons, while the form
|
||||
// container either shows forms or shows placeholder text.
|
||||
view! {
|
||||
@@ -234,94 +235,134 @@ pub fn LoanerPage() -> impl IntoView {
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-2">
|
||||
<Panel>{form_view}</Panel>
|
||||
<Panel>
|
||||
<Outlet />
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoanerFallback() -> impl IntoView {
|
||||
view! {
|
||||
<div
|
||||
class="flex flex-col items-center justify-center"
|
||||
use:animate=pop_in_out()
|
||||
>
|
||||
<div class="text-3xl font-bold mb-2">
|
||||
"Chromebook Loaner Form"
|
||||
</div>
|
||||
<div class="text-lg">
|
||||
"Select an option from the left to begin."
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoanerBorrowForm() -> impl IntoView {
|
||||
// Register server actions.
|
||||
let create_entry_action = ServerAction::<CreateBorrowEntry>::new();
|
||||
let navigate = use_navigate();
|
||||
|
||||
let temp = ExpandableInfo::new(uuid::Uuid::new_v4().to_string());
|
||||
let temp_reactive = temp.as_reactive();
|
||||
let elements_disabled = RwSignal::new(false);
|
||||
// Register server actions.
|
||||
let create_entry_action = ServerAction::<CreateBorrowEntry>::new();
|
||||
|
||||
let data = RwSignal::new(HashMap::new());
|
||||
let elements_disabled = Signal::derive(move || create_entry_action.pending().get());
|
||||
|
||||
let on_submit = move |_| {
|
||||
create_entry_action.dispatch(CreateBorrowEntry {
|
||||
input: ExpandableInfo {
|
||||
subject: uuid::Uuid::new_v4().to_string(),
|
||||
data: data.get(),
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//#region Toast Logic
|
||||
let mut toast_context = expect_context::<ToastContext>();
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(_)) = create_entry_action.value().get() {
|
||||
create_entry_action.clear();
|
||||
toast_context.toast(
|
||||
Toast::new()
|
||||
.id(uuid::Uuid::new_v4().to_string())
|
||||
.header("Borrower Added!")
|
||||
.msg("You can take your loaner now.")
|
||||
);
|
||||
navigate("/loaners", Default::default());
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
view! {
|
||||
<div class="flex flex-col justify-center gap-2 mb-1 py-3 px-2">
|
||||
<h2 class="text-2xl font-bold">"Borrower Information"</h2>
|
||||
<p class="text-lg">"Please fill out the form below."</p>
|
||||
</div>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member="first_name"
|
||||
data_map=temp_reactive.data
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member="last_name"
|
||||
data_map=temp_reactive.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Region 15 Email:"
|
||||
placeholder="example@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member="email"
|
||||
data_map=temp_reactive.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Select
|
||||
label="Collateral:"
|
||||
value_list=vec![
|
||||
"Phone".to_string(),
|
||||
"Earbuds/Headphones".to_string(),
|
||||
"Keys".to_string(),
|
||||
"Wallet".to_string(),
|
||||
"Laptop".to_string(),
|
||||
]
|
||||
disabled=elements_disabled
|
||||
data_member="collateral"
|
||||
data_map=temp_reactive.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Select
|
||||
label="Loan Taken:"
|
||||
value_list=vec![
|
||||
"Chromebook".to_string(),
|
||||
"Charger".to_string(),
|
||||
"Headphones".to_string(),
|
||||
]
|
||||
disabled=elements_disabled
|
||||
data_member="loan"
|
||||
data_map=temp_reactive.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
let captured = temp_reactive.capture();
|
||||
log!("Loaner record taken.");
|
||||
log!("{:?}", captured);
|
||||
create_entry_action.dispatch(CreateBorrowEntry {
|
||||
input: captured
|
||||
});
|
||||
navigate("/loaners", Default::default());
|
||||
}
|
||||
>"Submit"</ActionButton>
|
||||
</Row>
|
||||
<ValidatedForm
|
||||
use:animate=pop_in_out()
|
||||
on_submit=Callback::new(on_submit)
|
||||
title="Borrower Information"
|
||||
description="Please fill out the form below."
|
||||
>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member="first_name"
|
||||
data_map=data
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member="last_name"
|
||||
data_map=data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Region 15 Email:"
|
||||
placeholder="example@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member="email"
|
||||
data_map=data
|
||||
input_type=TextFieldType::Email(vec!["region15.org".to_string()])
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Select
|
||||
label="Collateral:"
|
||||
value_list=vec![
|
||||
"Phone".to_string(),
|
||||
"Earbuds/Headphones".to_string(),
|
||||
"Keys".to_string(),
|
||||
"Wallet".to_string(),
|
||||
"Laptop".to_string(),
|
||||
]
|
||||
disabled=elements_disabled
|
||||
data_member="collateral"
|
||||
data_map=data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Select
|
||||
label="Loan Taken:"
|
||||
value_list=vec![
|
||||
"Chromebook".to_string(),
|
||||
"Charger".to_string(),
|
||||
"Headphones".to_string(),
|
||||
]
|
||||
disabled=elements_disabled
|
||||
data_member="loan"
|
||||
data_map=data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
</ValidatedForm>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,12 +391,19 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
|
||||
// Create an effect to run after check-in - refreshes the list with another scan request.
|
||||
// This could be beneficial if we ever decided to make multiple loaner kiosks (think SAT days)
|
||||
let mut toasts = expect_context::<ToastContext>();
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(_)) = check_in_action.value().get() {
|
||||
// Return to the main page.
|
||||
if let Some(dialog) = dialog_ref.get() {
|
||||
dialog.close();
|
||||
}
|
||||
toasts.toast(
|
||||
Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.header("Check in successful!")
|
||||
.msg("Don't forget to take your collateral.")
|
||||
);
|
||||
navigate("/loaners", Default::default());
|
||||
check_in_action.clear();
|
||||
}
|
||||
@@ -381,6 +429,7 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<dialog
|
||||
use:animate=pop_in_out()
|
||||
class="p-3 m-auto rounded-md shadow-lg/33 backdrop:backdrop-blur-xs backdrop:transition-backdrop-filter"
|
||||
node_ref=dialog_ref>
|
||||
<h2 class="text-2xl font-bold">"Confirmation"</h2>
|
||||
@@ -396,12 +445,16 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
<ActionButton on:click=close_dialog disabled=dialog_disabled>"Close"</ActionButton>
|
||||
<ActionButton on:click=dialog_check_in disabled=dialog_disabled>"Check In"</ActionButton>
|
||||
</dialog>
|
||||
<div class="flex flex-col justify-center gap-2 mb-3 py-3 px-2">
|
||||
<h2 class="text-2xl font-bold">"Student List"</h2>
|
||||
<p class="text-lg">"Please select your name from the list."</p>
|
||||
</div>
|
||||
<Suspense fallback=move || "Loading...".into_view()>
|
||||
<div class="flex flex-col gap-2 m-2">
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-2 m-2"
|
||||
use:animate=pop_in_out()
|
||||
>
|
||||
<div class="flex flex-col justify-center gap-2 mb-3 py-3 px-2">
|
||||
<h2 class="text-2xl font-bold">"Student List"</h2>
|
||||
<p class="text-lg">"Please select your name from the list."</p>
|
||||
</div>
|
||||
<Suspense fallback=move || "Loading...".into_view()>
|
||||
{move || {
|
||||
match return_list_resource.get() {
|
||||
Some(loaner_list) => {
|
||||
@@ -443,7 +496,7 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
None => Either::Right("No loaners found.".into_view())
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
</Suspense>
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -8,8 +8,9 @@ mod scholarship_info;
|
||||
mod test_page;
|
||||
mod unauthenticated_page;
|
||||
mod utils;
|
||||
mod provider_contact;
|
||||
|
||||
pub use self::{
|
||||
about_page::*, comparison_test::*, home_page::*, loaner_page::*, provider_portal::*,
|
||||
scholarship_info::*, test_page::*, unauthenticated_page::*,
|
||||
test_page::*, unauthenticated_page::*, scholarship_info::*, provider_contact::*
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use crate::common::ValueType;
|
||||
use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
use crate::pages::utils::get_user_claims;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::pages::utils::server_utils::create_dynamo_client;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
|
||||
|
||||
#[server]
|
||||
async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
|
||||
use aws_sdk_dynamodb::types::AttributeValue;
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Getting contact info for provider {}", id);
|
||||
|
||||
match client
|
||||
.get_item()
|
||||
.table_name(PROVIDER_CONTACT_TABLE)
|
||||
.key("subject", AttributeValue::S(id))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
let Some(item) = output.item else {
|
||||
return Ok(HashMap::new());
|
||||
};
|
||||
|
||||
let map = item.iter().map(|(key, val)| {
|
||||
let val_type = ValueType::from(val);
|
||||
|
||||
(key.clone(), val_type)
|
||||
}).collect::<HashMap<String, ValueType>>();
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||
Err(ServerFnError::new(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> {
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
use crate::pages::utils::server_utils::into_attr_map;
|
||||
|
||||
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));
|
||||
|
||||
match client
|
||||
.put_item()
|
||||
.table_name(PROVIDER_CONTACT_TABLE)
|
||||
.set_item(Some(item))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||
Err(ServerFnError::new(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ProviderContactPage() -> impl IntoView {
|
||||
// Get user claims
|
||||
let user_claims = get_user_claims();
|
||||
let user_id = Memo::new(move |_| {
|
||||
user_claims.get()
|
||||
.map(|info| info.claims.subject.clone())
|
||||
});
|
||||
|
||||
// Get contact information
|
||||
let contact_info = RwSignal::new(HashMap::new());
|
||||
let contact_resource = Resource::new(
|
||||
move || user_id.get(),
|
||||
async move |id| {
|
||||
let Some(id) = id else {
|
||||
return Ok(HashMap::new());
|
||||
};
|
||||
|
||||
get_provider_contact(id).await
|
||||
}
|
||||
);
|
||||
|
||||
Effect::new(move || {
|
||||
match contact_resource.get() {
|
||||
Some(Ok(map)) => contact_info.set(map),
|
||||
Some(Err(e)) => log!("Error getting contact info: {}", e),
|
||||
None => log!("Contact info resource is not ready yet."),
|
||||
}
|
||||
});
|
||||
|
||||
// Set up submit action
|
||||
let submit_action = ServerAction::<PutProviderContact>::new();
|
||||
|
||||
let on_submit = move || {
|
||||
// Submit the form by reading the contact_info.
|
||||
submit_action.dispatch(PutProviderContact {
|
||||
id: user_id.get().unwrap_or_default(),
|
||||
contact_info: contact_info.get()
|
||||
});
|
||||
};
|
||||
|
||||
let elements_disabled = Signal::derive(move || {
|
||||
submit_action.pending().get()
|
||||
});
|
||||
|
||||
let mut toasts_context = expect_context::<ToastContext>();
|
||||
Effect::new(move || {
|
||||
submit_action.value().get().is_some()
|
||||
.then(|| {
|
||||
toasts_context.toast(
|
||||
Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.header("Submission Successful")
|
||||
.msg("You can go back or continue editing your responses.")
|
||||
);
|
||||
|
||||
submit_action.clear();
|
||||
});
|
||||
});
|
||||
|
||||
// Display contact form.
|
||||
view! {
|
||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/" />
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<Suspense fallback=Loading>
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<ValidatedForm
|
||||
on_submit=on_submit
|
||||
title="Contact Form"
|
||||
description="Enter your contact information and click submit."
|
||||
>
|
||||
{move || {
|
||||
contact_resource
|
||||
.get()
|
||||
.map(|_| {
|
||||
view! {
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member="first_name"
|
||||
data_map=contact_info
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member="last_name"
|
||||
data_map=contact_info
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=contact_info
|
||||
input_type=TextFieldType::Email(vec!["*".to_string()])
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Phone Number:"
|
||||
placeholder="123-456-7890"
|
||||
disabled=elements_disabled
|
||||
data_member="phone_number"
|
||||
data_map=contact_info
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Street Address:"
|
||||
placeholder="123 Fake Street"
|
||||
disabled=elements_disabled
|
||||
data_member="address"
|
||||
data_map=contact_info
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Testing Number"
|
||||
placeholder="Just put a number"
|
||||
disabled=elements_disabled
|
||||
data_member="testing_number"
|
||||
data_map=contact_info
|
||||
input_type=TextFieldType::Number
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</ValidatedForm>
|
||||
</Panel>
|
||||
<div class="flex flex-col flex-1" />
|
||||
</div>
|
||||
</Suspense>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ pub fn ProviderPortal() -> impl IntoView {
|
||||
title="Profile"
|
||||
description="Edit user profile"
|
||||
icon="/Person_Black.png"
|
||||
path="/"
|
||||
path="/providers/profile"
|
||||
/>
|
||||
<DashboardButton
|
||||
title="Create Scholarship"
|
||||
|
||||
+410
-308
@@ -1,24 +1,19 @@
|
||||
use super::UnauthenticatedPage;
|
||||
use super::api::{
|
||||
CreateScholarshipInfo, CreateTestComparisons, DeleteProviderScholarship, RegisterScholarship,
|
||||
get_comparison_info, get_provider_scholarships, get_scholarship_info,
|
||||
};
|
||||
use crate::common::ComparisonData;
|
||||
use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
||||
use crate::components::{
|
||||
ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row,
|
||||
};
|
||||
use crate::pages::utils::get_user_claims;
|
||||
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 traits::{AsReactive, ReactiveCapture};
|
||||
use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
||||
use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use super::UnauthenticatedPage;
|
||||
use crate::pages::utils::get_user_claims;
|
||||
use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized, CreateTestComparisons};
|
||||
|
||||
|
||||
/// # Scholarship Info Page
|
||||
/// This page will handle creating or editing scholarship information given a specific
|
||||
/// 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.
|
||||
@@ -29,65 +24,59 @@ pub fn ScholarshipInfoPage() -> impl IntoView {
|
||||
|
||||
let url_params = use_params::<ScholarshipFormParams>();
|
||||
let scholarship_id = Memo::new(move |_| {
|
||||
url_params
|
||||
.read()
|
||||
url_params.read()
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|params| params.id.clone())
|
||||
});
|
||||
|
||||
let create_test_comps = ServerAction::<CreateTestComparisons>::new();
|
||||
|
||||
|
||||
// let create_test_comps = ServerAction::<CreateTestComparisons>::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! {
|
||||
<Banner
|
||||
title="R15 Scholarships"
|
||||
logo="/PHS_Stacked_Acronym.png"
|
||||
path="/"
|
||||
/>
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<div class="flex flex-row align-top items-start">
|
||||
<div class="flex flex-col flex-1" />
|
||||
<ScholarshipList
|
||||
refresh_token=list_refresh
|
||||
on_delete=on_delete
|
||||
/>
|
||||
<Transition fallback=Loading>
|
||||
<ScholarshipForm
|
||||
scholarship_id=scholarship_id
|
||||
on_submit=move || {
|
||||
list_refresh.update(|n| *n += 1);
|
||||
}
|
||||
/>
|
||||
</Transition>
|
||||
<div class="flex flex-col flex-1" />
|
||||
</div>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/" />
|
||||
<ToastList>
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<div class="flex flex-row align-top items-start">
|
||||
<div class="flex flex-col flex-1" />
|
||||
<ScholarshipList refresh_token=list_refresh on_delete=on_delete />
|
||||
<Transition fallback=Loading>
|
||||
<ScholarshipForm
|
||||
scholarship_id=scholarship_id
|
||||
on_submit=move || {
|
||||
list_refresh.update(|n| *n += 1);
|
||||
}
|
||||
/>
|
||||
</Transition>
|
||||
<div class="flex flex-col flex-1" />
|
||||
</div>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
</ToastList>
|
||||
}
|
||||
}
|
||||
|
||||
/// # 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
|
||||
/// 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! {
|
||||
/// <AuthLoaded>
|
||||
/// <Authenticated>
|
||||
/// <ScholarshipList
|
||||
/// <ScholarshipList
|
||||
/// refresh_token=refresh
|
||||
/// on_delete=move || log!("This is run when an item is deleted on the server.")
|
||||
/// />
|
||||
@@ -99,11 +88,9 @@ pub fn ScholarshipInfoPage() -> impl IntoView {
|
||||
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<i32>,
|
||||
#[prop()] refresh_token: RwSignal<i32>,
|
||||
/// A callback that is run on successful server-side deletion of an item.
|
||||
#[prop(into)]
|
||||
on_delete: Callback<()>,
|
||||
#[prop(into)] on_delete: Callback<()>
|
||||
) -> impl IntoView {
|
||||
// List state
|
||||
let pending_delete = RwSignal::new(None::<ExpandableInfo>);
|
||||
@@ -112,43 +99,44 @@ fn ScholarshipList(
|
||||
// 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()
|
||||
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()),
|
||||
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::<RegisterScholarship>::new();
|
||||
let delete_action = ServerAction::<DeleteProviderScholarship>::new();
|
||||
|
||||
|
||||
// Button handlers
|
||||
let create_on_click = move |_| {
|
||||
create_action.dispatch(RegisterScholarship {
|
||||
provider_id: provider_id.get().unwrap(),
|
||||
scholarship_name: "Testing Scholarship".to_string(),
|
||||
scholarship_name: "Testing Scholarship".to_string()
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
@@ -179,47 +167,50 @@ fn ScholarshipList(
|
||||
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()
|
||||
dialog.show_modal()
|
||||
.expect("Couldn't show dialog. Is it already open?");
|
||||
} else {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
view! {
|
||||
<dialog
|
||||
node_ref=delete_dialog_ref
|
||||
class="m-auto p-2"
|
||||
>
|
||||
<dialog node_ref=delete_dialog_ref class="m-auto p-2">
|
||||
<h2 class="text-2xl font-bold">"Confirm Deletion"</h2>
|
||||
<p>
|
||||
"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("<no name found>".to_string())
|
||||
})
|
||||
}}
|
||||
"?"
|
||||
pending_delete
|
||||
.get()
|
||||
.map(|s| {
|
||||
s.data
|
||||
.get("name")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or("<no name found>".to_string())
|
||||
})
|
||||
}} "?"
|
||||
</p>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
let subject = pending_delete.get().map(|s| s.subject.clone()).unwrap_or_default();
|
||||
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()
|
||||
});
|
||||
delete_action
|
||||
.dispatch(DeleteProviderScholarship {
|
||||
scholarship_id: subject,
|
||||
provider_id: provider_id.get().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
disabled=modal_disabled
|
||||
>"Confirm"</ActionButton>
|
||||
<ActionButton
|
||||
on:click=move |_| pending_delete.set(None)
|
||||
>"Cancel"</ActionButton>
|
||||
>
|
||||
"Confirm"
|
||||
</ActionButton>
|
||||
<ActionButton on:click=move |_| pending_delete.set(None)>"Cancel"</ActionButton>
|
||||
</dialog>
|
||||
<div class="w-75">
|
||||
<Panel>
|
||||
@@ -229,31 +220,42 @@ fn ScholarshipList(
|
||||
<Transition fallback=Loading>
|
||||
<div class="flex flex-col gap-2">
|
||||
{move || {
|
||||
scholarships.get().map(|result| {
|
||||
match result {
|
||||
Ok(list) => {
|
||||
let views = list.iter().map(|entry| {
|
||||
view! {
|
||||
<ScholarshipListEntry scholarship=entry.clone()
|
||||
on_delete=Callback::new(on_item_delete)/>
|
||||
}
|
||||
});
|
||||
Either::Left(views.collect_view())
|
||||
scholarships
|
||||
.get()
|
||||
.map(|result| {
|
||||
match result {
|
||||
Ok(list) => {
|
||||
let views = list
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
view! {
|
||||
<ScholarshipListEntry
|
||||
scholarship=entry.clone()
|
||||
on_delete=Callback::new(on_item_delete)
|
||||
/>
|
||||
}
|
||||
});
|
||||
Either::Left(views.collect_view())
|
||||
}
|
||||
Err(err) => {
|
||||
Either::Right(
|
||||
view! {
|
||||
<div>
|
||||
<p>"Failed to load scholarship list: "{err.to_string()}</p>
|
||||
</div>
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
Either::Right(view! {
|
||||
<div>
|
||||
<p>"Failed to load scholarship list: "{err.to_string()}</p>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
}
|
||||
}).collect_view()
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
<div class="p-2 bg-red-800 rounded-md text-center text-white hover:bg-red-900 transition-all"
|
||||
on:click=create_on_click>
|
||||
<div
|
||||
class="p-2 bg-red-800 rounded-md text-center text-white hover:bg-red-900 transition-all cursor-pointer"
|
||||
on:click=create_on_click
|
||||
>
|
||||
"Create New"
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,9 +266,9 @@ fn ScholarshipList(
|
||||
}
|
||||
|
||||
/// # Scholarship List Entry Component
|
||||
///
|
||||
///
|
||||
/// Displays a single scholarship within the [`ScholarshipList`] component.
|
||||
///
|
||||
///
|
||||
/// Example usage (within the [`ScholarshipList`]):
|
||||
/// ```
|
||||
/// view! {
|
||||
@@ -280,26 +282,24 @@ fn ScholarshipList(
|
||||
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,
|
||||
#[prop()] scholarship: ExpandableInfo,
|
||||
/// A [`Callback<ExpandableInfo>`] that runs upon successful deletion of the scholarship.
|
||||
#[prop(into)]
|
||||
on_delete: Callback<ExpandableInfo, ()>,
|
||||
#[prop(into)] on_delete: Callback<ExpandableInfo, ()>,
|
||||
) -> 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
|
||||
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")
|
||||
let name = scholarship.data.get("name")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string()
|
||||
.unwrap_or_default()
|
||||
.as_string().unwrap_or_default()
|
||||
.unwrap_or("<no name found>".to_string());
|
||||
|
||||
|
||||
// Button click handlers
|
||||
let edit_click = {
|
||||
let subject = scholarship.subject.clone();
|
||||
@@ -308,20 +308,22 @@ fn ScholarshipListEntry(
|
||||
navigate(path.as_str(), Default::default());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
view! {
|
||||
<div class="flex flex-col rounded-md border-light inset-shadow-xs shadow-md my-2 transition-all hover:shadow-lg/33">
|
||||
<div class="p-2 text-center">
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="p-2 flex-1 bg-red-800 border-r border-white rounded-bl-md text-white text-center
|
||||
<div
|
||||
class="p-2 flex-1 bg-red-800 border-r border-white rounded-bl-md text-white text-center
|
||||
hover:bg-red-900 cursor-pointer transition-all"
|
||||
on:click=edit_click
|
||||
>
|
||||
"Edit"
|
||||
</div>
|
||||
<div class="p-2 flex-1 bg-red-800 border-l border-white rounded-br-md text-white text-center
|
||||
<div
|
||||
class="p-2 flex-1 bg-red-800 border-l border-white rounded-br-md text-white text-center
|
||||
hover:bg-red-900 cursor-pointer transition-all"
|
||||
on:click=move |_| on_delete.run(scholarship.clone())
|
||||
>
|
||||
@@ -354,54 +356,48 @@ fn ScholarshipListEntry(
|
||||
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<Option<String>>,
|
||||
#[prop(into)] scholarship_id: Signal<Option<String>>,
|
||||
/// 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<()>,
|
||||
#[prop(into)] on_submit: Callback<()>
|
||||
) -> 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::<CreateScholarshipInfo>::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(err.to_string())),
|
||||
},
|
||||
Err(_err) => submit_status.set(SubmitStatus::Error(()))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let comparison_list: Resource<Vec<ComparisonData>> = Resource::new(
|
||||
move || scholarship_id.get(), // There's no input to this function.
|
||||
let comparison_lists = Resource::new(
|
||||
move || scholarship_id.get().is_some(),
|
||||
async move |_| {
|
||||
get_comparison_info().await.unwrap_or_else(|e| {
|
||||
log!("Failed to get comparison info: {:?}", e);
|
||||
Vec::new()
|
||||
})
|
||||
},
|
||||
get_comparisons_categorized().await
|
||||
}
|
||||
);
|
||||
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(map)) = comparison_lists.get() {
|
||||
log!("Available categories: {:?}", map.keys());
|
||||
}
|
||||
});
|
||||
|
||||
let scholarship_info = Resource::new(
|
||||
move || scholarship_id.get(),
|
||||
async move |opt_id| {
|
||||
@@ -410,175 +406,281 @@ fn ScholarshipForm(
|
||||
} 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::<Vec<String>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
let comparison_text = Memo::new(move |_| {
|
||||
comparison_list
|
||||
.get()
|
||||
.map(|list| {
|
||||
list.iter()
|
||||
.map(|comp| comp.clone().display_text)
|
||||
.collect::<Vec<String>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
let get_comp_category = move |category: &str| {
|
||||
if let Some(Ok(map)) = comparison_lists.get() {
|
||||
map.get(category).unwrap_or(&Vec::new()).clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let get_comp_ids = move |category: &str| {
|
||||
get_comp_category(category)
|
||||
.into_iter()
|
||||
.map(|comp| comp.id)
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
|
||||
let get_comp_text = move |category: &str| {
|
||||
get_comp_category(category)
|
||||
.into_iter()
|
||||
.map(|comp| comp.display_text)
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
|
||||
// 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"));
|
||||
|
||||
let mut toasts = expect_context::<ToastContext>();
|
||||
|
||||
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::new(move || {
|
||||
submit_action.value().get().is_some()
|
||||
.then(|| {
|
||||
log!("Displaying toasts!");
|
||||
toasts.toast(
|
||||
Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.msg("Succesfully submitted.")
|
||||
);
|
||||
|
||||
submit_action.clear();
|
||||
});
|
||||
});
|
||||
|
||||
// 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
|
||||
let create_comps = ServerAction::<CreateTestComparisons>::new();
|
||||
|
||||
view! {
|
||||
<Panel>
|
||||
<Show
|
||||
when=move || scholarship_id.get().is_some()
|
||||
fallback=|| view! { <p>"Choose a scholarship from the right, or create a new one."</p> }
|
||||
fallback=|| {
|
||||
view! { <p>"Choose a scholarship from the right, or create a new one."</p> }
|
||||
}
|
||||
>
|
||||
<Suspense fallback=Loading>
|
||||
{move || {
|
||||
scholarship_info.get()
|
||||
.map(|res_scholarship| {
|
||||
match res_scholarship {
|
||||
Ok(scholarship) => {
|
||||
let reactive_info = scholarship.as_reactive();
|
||||
|
||||
Either::Left(view! {
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
"Scholarship Info Form (test)"
|
||||
</h1>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Scholarship Name"
|
||||
placeholder="Example Scholarship Name"
|
||||
disabled=elements_disabled
|
||||
data_member="name"
|
||||
data_map=reactive_info.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Amount per award: (if multiple, enter the highest value)"
|
||||
placeholder="Enter an amount..."
|
||||
disabled=elements_disabled
|
||||
data_member="amount_per_award"
|
||||
data_map=reactive_info.data
|
||||
input_type="number"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Total number of awards:"
|
||||
placeholder="Enter an amount..."
|
||||
disabled=elements_disabled
|
||||
data_member="num_awards"
|
||||
data_map=reactive_info.data
|
||||
input_type="number"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Total amount of all awards:"
|
||||
placeholder="Enter an amount..."
|
||||
disabled=elements_disabled
|
||||
data_member="total_awards"
|
||||
data_map=reactive_info.data
|
||||
input_type="number"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="fafsa_required"
|
||||
data_map=reactive_info.data
|
||||
items=vec!["Yes".to_string(), "No".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Do you require student financial information?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="transcript_required"
|
||||
data_map=reactive_info.data
|
||||
items=vec!["Yes".to_string(), "No".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Do you require a student transcript?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="award_to"
|
||||
data_map=reactive_info.data
|
||||
items=vec!["School".to_string(), "Student".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Will the award be made to the school or the student?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="essay_required"
|
||||
data_map=reactive_info.data
|
||||
items=vec!["Yes".to_string(), "No".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Do you require a student essay?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="essay_prompt"
|
||||
data_map=reactive_info.data
|
||||
items=vec!["Test 1", "Test 2", "Test 3"].iter().map(|s| s.to_string()).collect()
|
||||
disabled=elements_disabled
|
||||
label="If so, select a prompt from the list below."
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Scholarship Requirements"
|
||||
data_member="requirements"
|
||||
data_map=reactive_info.data
|
||||
values=comparison_ids
|
||||
displayed_text=comparison_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton
|
||||
disabled=elements_disabled
|
||||
on:click=move |_| {
|
||||
let captured = reactive_info.capture();
|
||||
log!("Map values: {:?}", captured);
|
||||
submit_action.dispatch(CreateScholarshipInfo {
|
||||
info: captured.clone()
|
||||
});
|
||||
}
|
||||
>"Submit"</ActionButton>
|
||||
</Row>
|
||||
<Row>
|
||||
<p>{result_msg}</p>
|
||||
</Row>
|
||||
})
|
||||
<ValidatedForm
|
||||
on_submit=Callback::new(on_submit)
|
||||
title="Scholarship Info Form"
|
||||
description="Create or edit a scholarship. Input all information and click Submit."
|
||||
>
|
||||
{move || {
|
||||
scholarship_info
|
||||
.get()
|
||||
.map(|res_scholarship| {
|
||||
match res_scholarship {
|
||||
Ok(_) => {
|
||||
Either::Left(
|
||||
view! {
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Scholarship Name"
|
||||
placeholder="Example Scholarship Name"
|
||||
disabled=elements_disabled
|
||||
data_member="name"
|
||||
data_map=form_data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Amount per award: (if multiple, enter the highest value)"
|
||||
placeholder="Enter an amount..."
|
||||
disabled=elements_disabled
|
||||
data_member="amount_per_award"
|
||||
data_map=form_data
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Total number of awards:"
|
||||
placeholder="Enter an amount..."
|
||||
disabled=elements_disabled
|
||||
data_member="num_awards"
|
||||
data_map=form_data
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Total amount of all award(s): (estimates are acceptable)"
|
||||
placeholder="Enter an amount..."
|
||||
disabled=elements_disabled
|
||||
data_member="total_awards"
|
||||
data_map=form_data
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="fafsa_required"
|
||||
data_map=form_data
|
||||
items=vec!["Yes".to_string(), "No".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Do you require student FAFSA information?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="transcript_required"
|
||||
data_map=form_data
|
||||
items=vec!["Yes".to_string(), "No".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Do you require a student transcript?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="award_to"
|
||||
data_map=form_data
|
||||
items=vec!["School".to_string(), "Student".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Will the award be made to the school or the student?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="essay_required"
|
||||
data_map=form_data
|
||||
items=vec!["Yes".to_string(), "No".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Do you require a student essay?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="essay_prompt"
|
||||
data_map=form_data
|
||||
items=vec!["Test 1", "Test 2", "Test 3"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
label="If so, select a prompt from the list below."
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="GPA Requirements"
|
||||
data_member="gpa"
|
||||
data_map=chips_data
|
||||
values=gpa_ids
|
||||
displayed_text=gpa_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Sports Participation"
|
||||
data_member="sports_participation"
|
||||
data_map=chips_data
|
||||
values=sports_ids
|
||||
displayed_text=sports_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Community Service"
|
||||
data_member="community_service"
|
||||
data_map=chips_data
|
||||
values=comm_ids
|
||||
displayed_text=comm_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Residency"
|
||||
data_member="residency"
|
||||
data_map=chips_data
|
||||
values=res_ids
|
||||
displayed_text=res_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Majors"
|
||||
data_member="major"
|
||||
data_map=chips_data
|
||||
values=major_ids
|
||||
displayed_text=major_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Additional Eligibility Factors"
|
||||
data_member="misc"
|
||||
data_map=chips_data
|
||||
values=misc_ids
|
||||
displayed_text=misc_text
|
||||
/>
|
||||
</Row>
|
||||
},
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
Either::Right(
|
||||
view! {
|
||||
<p>"Error while getting scholarship: "{err.to_string()}</p>
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
Either::Right(view! {
|
||||
<p>"Error while getting scholarship: "{err.to_string()}</p>
|
||||
})
|
||||
}
|
||||
}
|
||||
}).collect_view()
|
||||
}}
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
</ValidatedForm>
|
||||
<ActionButton on:click=move |_| {
|
||||
create_comps.dispatch(CreateTestComparisons {});
|
||||
}>"Create Comparisons"</ActionButton>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</Panel>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
@@ -9,7 +9,9 @@ use leptos_oidc::{Algorithm, AuthSignal, TokenData};
|
||||
/// dependencies cannot be compiled for WASM (nor should they be).
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod server_utils {
|
||||
use std::collections::HashMap;
|
||||
use aws_config::SdkConfig;
|
||||
use crate::common::ValueType;
|
||||
|
||||
/// Creates an [`SdkConfig`] struct for use with AWS SDK structs.
|
||||
pub async fn create_aws_config() -> SdkConfig {
|
||||
@@ -20,6 +22,10 @@ pub mod server_utils {
|
||||
pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client {
|
||||
aws_sdk_dynamodb::Client::new(&create_aws_config().await)
|
||||
}
|
||||
|
||||
pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> {
|
||||
map.into_iter().map(|(k, v)| (k, v.into())).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current user claims. This function should only be used in an area that has
|
||||
|
||||
Reference in new issue
Block a user