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)
+ }
+}