feat: implement new scholarship info form
This form requires more work, and the ChipsList component must also be altered to accept a list of value/display text pairs.
This commit is contained in:
1 parent
bfab0de564
commit
3bd9a7c6d5
7 files changed
+318
-8
No files matched your search
+2
-1
@@ -7,7 +7,7 @@ use leptos_router::{
|
|||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::pages::{AboutPage, ComparisonTestPage, HomePage};
|
use crate::pages::{AboutPage, ComparisonTestPage, HomePage, ScholarshipInfoPage};
|
||||||
|
|
||||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
@@ -68,6 +68,7 @@ pub fn App() -> impl IntoView {
|
|||||||
<Route path=StaticSegment("") view=HomePage />
|
<Route path=StaticSegment("") view=HomePage />
|
||||||
<Route path=StaticSegment("about") view=AboutPage />
|
<Route path=StaticSegment("about") view=AboutPage />
|
||||||
<Route path=StaticSegment("comparison") view=ComparisonTestPage />
|
<Route path=StaticSegment("comparison") view=ComparisonTestPage />
|
||||||
|
<Route path=StaticSegment("scholarship") view=ScholarshipInfoPage />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::{Comparison, NumberComparison};
|
|||||||
use crate::common::ValueType;
|
use crate::common::ValueType;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum NumberListComparison {
|
pub enum NumberListComparison {
|
||||||
/// Sums the values in the list and checks them using the given [`NumberComparison`].
|
/// Sums the values in the list and checks them using the given [`NumberComparison`].
|
||||||
/// Requires the list to contain only numbers.
|
/// Requires the list to contain only numbers.
|
||||||
@@ -65,7 +65,7 @@ impl Comparison for NumberListComparison {
|
|||||||
///
|
///
|
||||||
/// For example, using `TextListComparison::Contains` checks if a specific value is contained
|
/// For example, using `TextListComparison::Contains` checks if a specific value is contained
|
||||||
/// in the list.
|
/// in the list.
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum TextListComparison {
|
pub enum TextListComparison {
|
||||||
/// Checks if the list contains the target value.
|
/// Checks if the list contains the target value.
|
||||||
Contains,
|
Contains,
|
||||||
@@ -156,7 +156,7 @@ impl Comparison for TextListComparison {
|
|||||||
///
|
///
|
||||||
/// This will flatten a nested list into a single list of `ValueType::String` enums and then
|
/// This will flatten a nested list into a single list of `ValueType::String` enums and then
|
||||||
/// will check if the list contains the indicated target value.
|
/// will check if the list contains the indicated target value.
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum NestedListComparison {
|
pub enum NestedListComparison {
|
||||||
/// Flattens a nested list into a single text list, then performs the given comparison.
|
/// Flattens a nested list into a single text list, then performs the given comparison.
|
||||||
FlattenToTextList(Box<TextListComparison>),
|
FlattenToTextList(Box<TextListComparison>),
|
||||||
@@ -210,7 +210,7 @@ impl Comparison for NestedListComparison {
|
|||||||
/// ```
|
/// ```
|
||||||
/// MapListComparison::FlattenToTextList("name".to_owned(), Box::new(TextListComparison::Equal));
|
/// MapListComparison::FlattenToTextList("name".to_owned(), Box::new(TextListComparison::Equal));
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum MapListComparison {
|
pub enum MapListComparison {
|
||||||
/// Flattens the map list to a text list and then performs the given comparison.
|
/// Flattens the map list to a text list and then performs the given comparison.
|
||||||
FlattenToTextList(String, Box<TextListComparison>),
|
FlattenToTextList(String, Box<TextListComparison>),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::Comparison;
|
|||||||
use crate::common::ValueType;
|
use crate::common::ValueType;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum NumberComparison {
|
pub enum NumberComparison {
|
||||||
GreaterThan,
|
GreaterThan,
|
||||||
LessThan,
|
LessThan,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::Comparison;
|
|||||||
use crate::common::ValueType;
|
use crate::common::ValueType;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum TextComparison {
|
pub enum TextComparison {
|
||||||
Matches,
|
Matches,
|
||||||
NotMatches,
|
NotMatches,
|
||||||
|
|||||||
+3
-1
@@ -2,5 +2,7 @@ mod about_page;
|
|||||||
mod comparison_test;
|
mod comparison_test;
|
||||||
mod home_page;
|
mod home_page;
|
||||||
mod unauthenticated_page;
|
mod unauthenticated_page;
|
||||||
|
mod scholarship_info;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
pub use self::{about_page::*, comparison_test::*, home_page::*, unauthenticated_page::*};
|
pub use self::{about_page::*, comparison_test::*, home_page::*, unauthenticated_page::*, scholarship_info::*};
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use super::utils::server_utils::create_dynamo_client;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use aws_sdk_dynamodb::{
|
||||||
|
error::ProvideErrorMetadata,
|
||||||
|
types::AttributeValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
use leptos::logging::log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||||
|
use crate::common::{ComparisonType, ExpandableInfo, NumberComparison, ValueType};
|
||||||
|
use crate::components::{ActionButton, ChipsList, Loading, OutlinedTextField, Panel, Row};
|
||||||
|
use super::UnauthenticatedPage;
|
||||||
|
use traits::{AsReactive, ReactiveCapture};
|
||||||
|
use crate::common::ComparisonData;
|
||||||
|
|
||||||
|
#[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")]
|
||||||
|
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
// Perform the operation - we just want to return all data that's contained in this entry,
|
||||||
|
// or just return an empty ExpandableInfo struct.
|
||||||
|
log!("Getting scholarship info using id {:?}", id);
|
||||||
|
match client
|
||||||
|
.get_item()
|
||||||
|
.table_name("leptos-scholarship-test")
|
||||||
|
.key("subject", AttributeValue::S(id.clone()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(output) => {
|
||||||
|
if let Some(item) = output.item {
|
||||||
|
log!("Found output from API: {:?}", item);
|
||||||
|
Ok(serde_dynamo::from_item(item)?)
|
||||||
|
} else {
|
||||||
|
log!("Couldn't find any values, returning default struct.");
|
||||||
|
Ok(ExpandableInfo::new(id.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
Err(ServerFnError::new(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(CreateScholarshipInfo, endpoint = "/scholarship/info/create")]
|
||||||
|
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> {
|
||||||
|
use super::utils::server_utils::create_dynamo_client;
|
||||||
|
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
log!("Creating or updating scholarship with ID {:?}", info.subject);
|
||||||
|
|
||||||
|
match client
|
||||||
|
.put_item()
|
||||||
|
.table_name("leptos-scholarship-test")
|
||||||
|
.set_item(Some(serde_dynamo::to_item(&info)?))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
Err(ServerFnError::new(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError> {
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
log!("Getting all comparisons from the database");
|
||||||
|
|
||||||
|
// 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("leptos-comparison-test")
|
||||||
|
.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![])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
Err(ServerFnError::new(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(CreateTestComparisons, endpoint = "/comparisons/create-test")]
|
||||||
|
async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
log!("Creating test comparisons");
|
||||||
|
|
||||||
|
let test_comp = ComparisonData::new(
|
||||||
|
"comp1",
|
||||||
|
"unweighted_gpa",
|
||||||
|
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
|
||||||
|
ValueType::Number(Some(3.2.to_string())),
|
||||||
|
"Testing",
|
||||||
|
"GPA > 3.2"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _comp_list = vec![
|
||||||
|
ComparisonData::new(
|
||||||
|
"comp1",
|
||||||
|
"unweighted_gpa",
|
||||||
|
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
|
||||||
|
ValueType::Number(Some(3.2.to_string())),
|
||||||
|
"Testing",
|
||||||
|
"GPA > 3.2"
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
match client
|
||||||
|
.put_item()
|
||||||
|
.table_name("leptos-comparison-test")
|
||||||
|
.set_item(Some(serde_dynamo::to_item(&test_comp)?))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||||
|
Err(ServerFnError::new(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Scholarship Info Page
|
||||||
|
/// This page will handle creating or editing scholarship information given a specific
|
||||||
|
/// scholarship ID and scholarship provider subject number. The form itself will only
|
||||||
|
/// track a single scholarship (for now) and so selection of what scholarship will be edited
|
||||||
|
/// must be handled *before* navigation to this page.
|
||||||
|
///
|
||||||
|
/// In its current testing form, we'll be using a single fixed ID. This absolutely *MUST* be
|
||||||
|
/// changed before a full release.
|
||||||
|
#[component]
|
||||||
|
pub fn ScholarshipInfoPage() -> impl IntoView {
|
||||||
|
let scholarship_id = String::from("test-scholarship-id");
|
||||||
|
|
||||||
|
let get_scholarship_action: Resource<ExpandableInfo> = Resource::new(
|
||||||
|
move || scholarship_id.clone(),
|
||||||
|
async |id| {
|
||||||
|
get_scholarship_info(id).await
|
||||||
|
// Note that this really should notify us of an error and then redirect or ask
|
||||||
|
// the user to try again.
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
log!("Failed to get scholarship info: {:?}", e);
|
||||||
|
log!("Using default ExpandableInfo");
|
||||||
|
ExpandableInfo::new("test-scholarship-id")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let get_comparison_info: Resource<Vec<ComparisonData>> = Resource::new(
|
||||||
|
move || Option::<String>::None, // There's no input to this function.
|
||||||
|
async |_| {
|
||||||
|
get_comparison_info().await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
log!("Failed to get comparison info: {:?}", e);
|
||||||
|
Vec::new()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let create_scholarship_action = ServerAction::<CreateScholarshipInfo>::new();
|
||||||
|
let create_test_comps = ServerAction::<CreateTestComparisons>::new();
|
||||||
|
|
||||||
|
// Remember that we only want to show the page after we've got authentication information
|
||||||
|
// and if there's data for us to use.
|
||||||
|
view! {
|
||||||
|
<AuthLoaded fallback=Loading>
|
||||||
|
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||||
|
<Suspense fallback=Loading>
|
||||||
|
{move || Suspend::new(async move {
|
||||||
|
let response = get_scholarship_action.get();
|
||||||
|
let comparisons = get_comparison_info.get();
|
||||||
|
|
||||||
|
response
|
||||||
|
.zip(comparisons)
|
||||||
|
.map(|(response, comparison_list)| view! {
|
||||||
|
<ScholarshipForm
|
||||||
|
response=response
|
||||||
|
comparison_list=comparison_list
|
||||||
|
submit_action=create_scholarship_action
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
.collect_view()
|
||||||
|
})}
|
||||||
|
<Row>
|
||||||
|
<ActionButton
|
||||||
|
on:click=move |_| {
|
||||||
|
create_test_comps.dispatch(CreateTestComparisons {});
|
||||||
|
}
|
||||||
|
>"Create test comparisons"</ActionButton>
|
||||||
|
</Row>
|
||||||
|
</Suspense>
|
||||||
|
</Authenticated>
|
||||||
|
</AuthLoaded>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ScholarshipForm(
|
||||||
|
response: ExpandableInfo,
|
||||||
|
comparison_list: Vec<ComparisonData>,
|
||||||
|
submit_action: ServerAction<CreateScholarshipInfo>
|
||||||
|
) -> impl IntoView {
|
||||||
|
let reactive_info = response.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_string()
|
||||||
|
} else {
|
||||||
|
elements_disabled.set(false);
|
||||||
|
if let Some(result) = submit_action.value().get() {
|
||||||
|
match result {
|
||||||
|
Ok(()) => "Request sent successfully".to_string(),
|
||||||
|
Err(err) => {
|
||||||
|
format!("An error occurred: {:?}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Panel>
|
||||||
|
<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>
|
||||||
|
<ChipsList
|
||||||
|
label="Scholarship Requirements"
|
||||||
|
data_member="requirements"
|
||||||
|
data_map=reactive_info.data
|
||||||
|
items=comparison_list
|
||||||
|
.into_iter()
|
||||||
|
.map(|comp| comp.id)
|
||||||
|
.collect()
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<ActionButton
|
||||||
|
disabled=elements_disabled
|
||||||
|
on:click=move |_| {
|
||||||
|
let captured = reactive_info.capture();
|
||||||
|
log!("Map values: {:?}", captured);
|
||||||
|
submit_action.dispatch(CreateScholarshipInfo {
|
||||||
|
info: captured
|
||||||
|
});
|
||||||
|
}
|
||||||
|
>"Submit"</ActionButton>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<p>{result_msg}</p>
|
||||||
|
</Row>
|
||||||
|
</Panel>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/// # Server Utilities
|
||||||
|
///
|
||||||
|
/// These utility functions can only be used from the server. They are created to use server-side
|
||||||
|
/// dependencies like the AWS SDK. Use of these functions will crash the front end, as these
|
||||||
|
/// dependencies cannot be compiled for WASM (nor should they be).
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod server_utils {
|
||||||
|
use aws_config::SdkConfig;
|
||||||
|
|
||||||
|
/// Creates an [`SdkConfig`] struct for use with AWS SDK structs.
|
||||||
|
pub async fn create_aws_config() -> SdkConfig {
|
||||||
|
aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a DynamoDB client.
|
||||||
|
pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client {
|
||||||
|
aws_sdk_dynamodb::Client::new(&create_aws_config().await)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user