diff --git a/src/app.rs b/src/app.rs index 84079ef..5673926 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,7 +7,7 @@ use leptos_router::{ }; use url::Url; -use crate::pages::{AboutPage, ComparisonTestPage, HomePage}; +use crate::pages::{AboutPage, ComparisonTestPage, HomePage, ScholarshipInfoPage}; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -68,6 +68,7 @@ pub fn App() -> impl IntoView { + diff --git a/src/common/comparison/lists.rs b/src/common/comparison/lists.rs index c335dd6..ff6112c 100644 --- a/src/common/comparison/lists.rs +++ b/src/common/comparison/lists.rs @@ -2,7 +2,7 @@ use super::{Comparison, NumberComparison}; use crate::common::ValueType; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum NumberListComparison { /// Sums the values in the list and checks them using the given [`NumberComparison`]. /// 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 /// in the list. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum TextListComparison { /// Checks if the list contains the target value. 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 /// will check if the list contains the indicated target value. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum NestedListComparison { /// Flattens a nested list into a single text list, then performs the given comparison. FlattenToTextList(Box), @@ -210,7 +210,7 @@ impl Comparison for NestedListComparison { /// ``` /// MapListComparison::FlattenToTextList("name".to_owned(), Box::new(TextListComparison::Equal)); /// ``` -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum MapListComparison { /// Flattens the map list to a text list and then performs the given comparison. FlattenToTextList(String, Box), diff --git a/src/common/comparison/number.rs b/src/common/comparison/number.rs index 6a51880..3440e97 100644 --- a/src/common/comparison/number.rs +++ b/src/common/comparison/number.rs @@ -2,7 +2,7 @@ use super::Comparison; use crate::common::ValueType; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum NumberComparison { GreaterThan, LessThan, diff --git a/src/common/comparison/text.rs b/src/common/comparison/text.rs index 4079de3..23b4d30 100644 --- a/src/common/comparison/text.rs +++ b/src/common/comparison/text.rs @@ -2,7 +2,7 @@ use super::Comparison; use crate::common::ValueType; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum TextComparison { Matches, NotMatches, diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 7fe2f6d..b30030c 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -2,5 +2,7 @@ mod about_page; mod comparison_test; mod home_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::*}; diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs new file mode 100644 index 0000000..6012b7e --- /dev/null +++ b/src/pages/scholarship_info.rs @@ -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 { + 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, 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 = 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> = Resource::new( + move || Option::::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::::new(); + let create_test_comps = ServerAction::::new(); + + // Remember that we only want to show the page after we've got authentication information + // and if there's data for us to use. + view! { + + + + {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! { + + }) + .collect_view() + })} + + "Create test comparisons" + + + + + } +} + +#[component] +fn ScholarshipForm( + response: ExpandableInfo, + comparison_list: Vec, + submit_action: ServerAction +) -> 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! { + + +

+ "Scholarship Info Form (test)" +

+
+ + + + + + + + "Submit" + + +

{result_msg}

+
+
+ } +} diff --git a/src/pages/utils.rs b/src/pages/utils.rs new file mode 100644 index 0000000..b12d842 --- /dev/null +++ b/src/pages/utils.rs @@ -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) + } +}