}
}
diff --git a/src/pages/api/scholarships.rs b/src/pages/api/scholarships.rs
index edc7b4c..817e83a 100644
--- a/src/pages/api/scholarships.rs
+++ b/src/pages/api/scholarships.rs
@@ -7,7 +7,8 @@ use aws_sdk_dynamodb::{
use leptos::leptos_dom::log;
use leptos::prelude::ServerFnError;
use leptos::server;
-use crate::common::ExpandableInfo;
+use uuid::Uuid;
+use crate::common::{ExpandableInfo, ValueType};
#[cfg(feature = "ssr")]
use crate::pages::utils::server_utils::create_dynamo_client;
@@ -89,3 +90,77 @@ pub async fn get_all_scholarship_info() -> Result, 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, ServerFnError> {
+ let client = create_dynamo_client().await;
+
+ 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)))?)
+ .filter_expression("provider_id = :id")
+ .send()
+ .await
+ {
+ Ok(output) => {
+ if let Some(items) = output.items {
+ Ok(serde_dynamo::from_items(items)?)
+ } else {
+ Ok(Vec::new())
+ }
+ }
+ Err(err) => {
+ let msg = err.message().unwrap_or("An unknown error occurred");
+ Err(ServerFnError::new(msg))
+ }
+ }
+}
+
+/// 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 {
+ let client = create_dynamo_client().await;
+
+ 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)));
+ loop {
+ let ser_item = serde_dynamo::to_item(&item)?;
+ match client
+ .put_item()
+ .table_name("leptos-scholarship-test")
+ .set_item(Some(ser_item))
+ .condition_expression("attribute_not_exists(subject)")
+ .send()
+ .await
+ {
+ Ok(_) => {
+ // Return the uuid that we used.
+ return Ok(current_uuid);
+ }
+ Err(err) => {
+ // Check the error to figure out if it was a key validation error.
+ // If so, retry with new values.
+ match err.code() {
+ Some("ConditionalCheckFailedException") => {
+ log!("Retrying conditional check.");
+ log!("Current error: {:?}", err);
+ current_uuid = Uuid::new_v4().to_string();
+ item.subject = current_uuid.clone();
+ continue
+ }
+ _ => {
+ return Err(ServerFnError::new(err.message().unwrap_or("An unknown error occurred")));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/pages/comparison_test.rs b/src/pages/comparison_test.rs
index c08f9f4..94dc7c1 100644
--- a/src/pages/comparison_test.rs
+++ b/src/pages/comparison_test.rs
@@ -2,26 +2,20 @@ use leptos::either::Either;
use super::home_page::get_submission;
use super::{UnauthenticatedPage};
use crate::common::{
- ComparisonData, ExpandableInfo, UserClaims, ValueType,
+ ComparisonData, ExpandableInfo, ValueType,
};
use crate::components::{ActionButton, Loading, Panel, Row};
use leptos::leptos_dom::logging::console_log;
use leptos::logging::log;
use leptos::prelude::*;
-use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
+use leptos_oidc::{AuthLoaded, Authenticated};
+use crate::pages::utils::get_user_claims;
use super::api::{get_comparison_info, get_all_scholarship_info};
#[component]
pub fn ComparisonTestPage() -> impl IntoView {
let result_msg = RwSignal::new("Found submission, waiting for input.".to_string());
- let auth = use_context::().expect("Couldn't find user information.");
- let user_claims = Signal::derive(move || {
- auth.with(|auth| {
- auth.authenticated().and_then(|data| {
- data.decoded_access_token::(Algorithm::RS256, &["account"])
- })
- })
- });
+ let user_claims = get_user_claims();
// 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
diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs
index 4c6ddb9..19c41ea 100644
--- a/src/pages/home_page.rs
+++ b/src/pages/home_page.rs
@@ -9,7 +9,7 @@ use std::process::{Command, Stdio};
#[cfg(feature = "ssr")]
use std::io::Write;
-use crate::common::{ExpandableInfo, UserClaims};
+use crate::common::ExpandableInfo;
use crate::pages::UnauthenticatedPage;
use crate::components::{
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
@@ -18,12 +18,13 @@ use crate::components::{
use crate::input;
use leptos::leptos_dom::logging::console_log;
use leptos::prelude::*;
-use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
+use leptos_oidc::{AuthLoaded, Authenticated};
use base64::Engine;
use leptos::task::spawn_local;
use leptos::web_sys::HtmlAnchorElement;
use leptos::wasm_bindgen::JsCast;
use traits::{AsReactive, ReactiveCapture};
+use crate::pages::utils::get_user_claims;
/// # Get Student Info
/// Gets a student's information given their `subject`.
@@ -146,15 +147,7 @@ pub async fn create_example_pdf() -> Result, ServerFnError> {
#[component]
pub fn HomePage() -> impl IntoView {
// Creates a reactive value to update the button
-
- let auth = use_context::().expect("Couldn't find user information.");
- let user_claims = Signal::derive(move || {
- auth.with(|auth| {
- auth.authenticated().and_then(|data| {
- data.decoded_access_token::(Algorithm::RS256, &["account"])
- })
- })
- });
+ let user_claims = get_user_claims();
// 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
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 6c06aaa..866fb60 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -11,5 +11,5 @@ mod loaner_page;
pub use self::{
about_page::*, home_page::*, provider_portal::*, test_page::*, unauthenticated_page::*,
- loaner_page::LoanerPage, comparison_test::*,
+ loaner_page::*, comparison_test::*, scholarship_info::*,
};
diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs
index 63bb3ab..f737843 100644
--- a/src/pages/scholarship_info.rs
+++ b/src/pages/scholarship_info.rs
@@ -1,13 +1,15 @@
+use leptos::either::Either;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_oidc::{AuthLoaded, Authenticated};
-use leptos_router::hooks::use_params;
-use crate::common::{ExpandableInfo, ScholarshipFormParams};
-use crate::components::{ActionButton, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row};
+use leptos_router::hooks::{use_navigate, use_params};
+use crate::common::{ExpandableInfo, ScholarshipFormParams, ValueType};
+use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row};
use super::UnauthenticatedPage;
use traits::{AsReactive, ReactiveCapture};
use crate::common::ComparisonData;
-use super::api::{get_comparison_info, get_scholarship_info, CreateScholarshipInfo, CreateTestComparisons};
+use crate::pages::utils::get_user_claims;
+use super::api::{get_comparison_info, get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, CreateTestComparisons, RegisterScholarship};
/// # Scholarship Info Page
@@ -15,12 +17,8 @@ use super::api::{get_comparison_info, get_scholarship_info, CreateScholarshipInf
/// 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-id2");
let url_params = use_params::();
let scholarship_id = move || {
url_params.read()
@@ -61,11 +59,17 @@ pub fn ScholarshipInfoPage() -> impl IntoView {
// 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! {
+
-
+
-
+
}
}
+#[component]
+fn ScholarshipList() -> impl IntoView {
+ // 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()
+ .as_ref()
+ .map(|info| info.claims.subject.clone())
+ );
+
+ // Register scholarship server resource
+ let scholarships = Resource::new(
+ move || provider_id,
+ async move |provider_id| {
+ let Some(provider_id) = provider_id.get() else {
+ return Ok(Vec::new());
+ };
+
+ // Get the scholarships
+ get_provider_scholarships(provider_id).await
+ }
+ );
+
+ // Register scholarship creation action
+ let create_action = ServerAction::::new();
+
+ // Button handlers
+ let create_on_click = move |_| {
+ // In the future, we'd actually like to create/show a dialog that gets the
+ // desired name of the scholarship.
+ // After the request is sent, we'll handle it in an effect.
+ create_action.dispatch(RegisterScholarship {
+ provider_id: provider_id.get().unwrap(),
+ scholarship_name: "Testing Scholarship".to_string()
+ });
+ };
+
+ // Register scholarship creation effect
+ Effect::new(move || {
+ // We want to wait for the action to complete first, but then find whether the request
+ // succeeded.
+ if let Some(Ok(_)) = create_action.value().get() {
+ // Refetch and clear.
+ scholarships.refetch();
+ create_action.clear();
+ }
+ });
+
+ view! {
+
"Failed to load scholarship list: "{err.to_string()}
+
+ })
+ }
+ }
+ }).collect_view()
+ }}
+
+
+
+ "Create New"
+
+
+
+
+
+ }
+}
+
+#[component]
+fn ScholarshipListEntry(scholarship: ExpandableInfo) -> impl IntoView {
+ let navigate = use_navigate();
+
+ // 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("scholarship_name")
+ .unwrap_or(&ValueType::String(None))
+ .as_string().unwrap_or_default()
+ .unwrap_or("".to_string());
+
+ // Button click handlers
+ let edit_click = {
+ let subject = scholarship.subject.clone();
+ move |_| {
+ let path = format!("/providers/scholarships/{}", subject);
+ navigate(path.as_str(), Default::default());
+ }
+ };
+
+ let delete_click = {
+ let subject = scholarship.subject.clone();
+ move |_| {
+ // Delete the scholarship. For now, we'll just log it.
+ log!("Deleting scholarship with ID {:?}", subject);
+ }
+ };
+
+ view! {
+
+
+ {name}
+
+
+
+ "Edit"
+
+
+ "Delete"
+
+
+
+ }
+}
+
#[component]
fn ScholarshipForm(
response: ExpandableInfo,
diff --git a/src/pages/utils.rs b/src/pages/utils.rs
index b12d842..1ae07b9 100644
--- a/src/pages/utils.rs
+++ b/src/pages/utils.rs
@@ -1,3 +1,7 @@
+use leptos::prelude::{use_context, Signal, With};
+use leptos_oidc::{Algorithm, AuthSignal, TokenData};
+use crate::common::UserClaims;
+
/// # Server Utilities
///
/// These utility functions can only be used from the server. They are created to use server-side
@@ -17,3 +21,16 @@ pub mod server_utils {
aws_sdk_dynamodb::Client::new(&create_aws_config().await)
}
}
+
+/// Gets the current user claims. This function should only be used in an area that has
+/// access to an AuthSignal, or it will result in a total failure.
+pub fn get_user_claims() -> Signal