diff --git a/src/app.rs b/src/app.rs
index c51ccdc..b3b1aa6 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,12 +1,9 @@
-use crate::pages::{AboutPage, ComparisonTestPage, HomePage, LoanerPage, ProviderPortal, TestPage};
+use crate::pages::{AboutPage, HomePage, LoanerPage, ProviderPortal, ComparisonTestPage, ScholarshipInfoPage, TestPage};
use leptos::leptos_dom::log;
use leptos::prelude::*;
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
use leptos_oidc::{Auth, AuthParameters, AuthSignal, Challenge};
-use leptos_router::{
- components::{Route, Router, Routes},
- path,
-};
+use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@@ -35,7 +32,7 @@ pub fn App() -> impl IntoView {
// injects a stylesheet into the document
// id=leptos means cargo-leptos will hot-reload this stylesheet
-
+
// sets the document title
@@ -47,8 +44,7 @@ pub fn App() -> impl IntoView {
}
fn use_origin() -> String {
- #[cfg(target_arch = "wasm32")]
- {
+ #[cfg(target_arch = "wasm32")] {
use leptos::web_sys;
// Just get the current URL origin.
web_sys::window()
@@ -57,9 +53,8 @@ fn use_origin() -> String {
.origin()
.unwrap_or("http://localhost:3000/".to_string())
}
-
- #[cfg(not(target_arch = "wasm32"))]
- {
+
+ #[cfg(not(target_arch = "wasm32"))] {
// Read the expected origin out of an environment variable.
std::env::var("LP_SITE_ORIGIN").unwrap_or("http://localhost:3000/".to_string())
}
@@ -68,7 +63,7 @@ fn use_origin() -> String {
#[component]
pub fn AppWithRoutes() -> impl IntoView {
provide_meta_context();
-
+
let current_origin = use_origin();
log!("Current origin: {}", current_origin);
@@ -87,7 +82,7 @@ pub fn AppWithRoutes() -> impl IntoView {
let auth = Auth::signal();
provide_context(auth); // allows use of this signal in lower areas of the tree without
- // explicitly passing it through the html tree
+ // explicitly passing it through the html tree
let _ = Auth::init(parameters);
@@ -101,17 +96,23 @@ pub fn AppWithRoutes() -> impl IntoView {
// sets the document title
-
+
// TODO Create a 404 page
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
}
diff --git a/src/common/expandable_info.rs b/src/common/expandable_info.rs
index a209606..0ef8ff0 100644
--- a/src/common/expandable_info.rs
+++ b/src/common/expandable_info.rs
@@ -15,7 +15,7 @@ use std::collections::HashMap;
/// now, the table will simply create a single massive `Map` field that contains all the stored
/// information in the `HashMap`. We'd prefer to be able to get single values if possible,
/// especially for interoperability with other applications, should they be created.
-#[derive(Debug, Clone, Serialize, Deserialize, Reactive)]
+#[derive(Debug, Clone, Serialize, Deserialize, Reactive, )]
pub struct ExpandableInfo {
/// The main key member. Required and used to get/put the information in a database.
pub subject: String,
diff --git a/src/common/mod.rs b/src/common/mod.rs
index 135600c..a34a9ec 100644
--- a/src/common/mod.rs
+++ b/src/common/mod.rs
@@ -4,7 +4,6 @@ mod form_params;
mod form_schema;
mod user;
mod value_type;
+mod submit_status;
-pub use self::{
- comparison::*, expandable_info::*, form_params::*, form_schema::*, user::*, value_type::*,
-};
+pub use self::{comparison::*, expandable_info::*, form_schema::*, user::*, value_type::*, form_params::*, submit_status::*};
diff --git a/src/common/submit_status.rs b/src/common/submit_status.rs
new file mode 100644
index 0000000..c6b8a90
--- /dev/null
+++ b/src/common/submit_status.rs
@@ -0,0 +1,7 @@
+#[derive(Debug, Clone)]
+pub enum SubmitStatus {
+ Idle,
+ Sending,
+ Success,
+ Error(String),
+}
diff --git a/src/components/action_button.rs b/src/components/action_button.rs
index 475dca0..8d9728e 100644
--- a/src/components/action_button.rs
+++ b/src/components/action_button.rs
@@ -2,8 +2,8 @@ use leptos::prelude::*;
#[component]
pub fn ActionButton(
- #[prop(optional, default = String::from("button"))] button_type: String,
- #[prop(optional, default = RwSignal::new(false))] disabled: RwSignal,
+ #[prop(default = String::from("button"))] button_type: String,
+ #[prop(optional, into)] disabled: Signal,
children: Children,
) -> impl IntoView {
view! {
diff --git a/src/components/chips.rs b/src/components/chips.rs
index 6c117b0..bf57241 100644
--- a/src/components/chips.rs
+++ b/src/components/chips.rs
@@ -70,8 +70,8 @@ pub fn Chip(
pub fn ChipsList(
#[prop(into)] data_member: String,
#[prop()] data_map: RwSignal>,
- #[prop()] displayed_text: Vec,
- #[prop()] values: Vec,
+ #[prop(into)] displayed_text: Signal>,
+ #[prop(into)] values: Signal>,
#[prop(default = RwSignal::new(false))] disabled: RwSignal,
#[prop(optional, into)] label: String,
) -> impl IntoView {
@@ -79,7 +79,7 @@ pub fn ChipsList(
{label}
- {displayed_text.into_iter().zip(values)
+ {move || { displayed_text.get().into_iter().zip(values.get())
.into_iter()
.map(|(text, value)| {
let checked_signal = Signal::derive({
@@ -140,7 +140,7 @@ pub fn ChipsList(
}
})
.collect_view()
- }
+ }}
}
diff --git a/src/components/loading.rs b/src/components/loading.rs
index caa82be..53f2ab3 100644
--- a/src/components/loading.rs
+++ b/src/components/loading.rs
@@ -4,7 +4,7 @@ use leptos::prelude::*;
pub fn Loading() -> impl IntoView {
view! {
-

+
}
}
diff --git a/src/components/radio.rs b/src/components/radio.rs
index 72047ef..fd8af70 100644
--- a/src/components/radio.rs
+++ b/src/components/radio.rs
@@ -9,7 +9,7 @@ pub fn Radio(
#[prop(into)] on_change: Callback<(), ()>,
#[prop(into)] value: String,
#[prop(into)] name: String,
- #[prop(default = RwSignal::new(false))] disabled: RwSignal,
+ #[prop(optional, into)] disabled: Signal,
) -> impl IntoView {
let id = create_unique_id(&name, &value);
@@ -53,7 +53,7 @@ pub fn RadioList(
#[prop(into)] data_member: String,
#[prop()] data_map: RwSignal>,
#[prop()] items: Vec,
- #[prop(default = RwSignal::new(false))] disabled: RwSignal,
+ #[prop(optional, into)] disabled: Signal,
#[prop(optional, into)] label: String,
) -> impl IntoView {
view! {
diff --git a/src/components/text_field.rs b/src/components/text_field.rs
index 330a3ea..4dae16c 100644
--- a/src/components/text_field.rs
+++ b/src/components/text_field.rs
@@ -11,7 +11,7 @@ pub fn OutlinedTextField(
#[prop()] data_map: RwSignal>,
#[prop(optional)] value: RwSignal,
#[prop(default = "text".to_owned(), into)] input_type: String,
- #[prop(optional)] disabled: RwSignal,
+ #[prop(into, optional)] disabled: Signal,
#[prop(optional)] error: RwSignal,
#[prop(optional, into)] name: String,
#[prop(optional, into)] label: String,
diff --git a/src/pages/api/scholarships.rs b/src/pages/api/scholarships.rs
index 4ec5301..b006143 100644
--- a/src/pages/api/scholarships.rs
+++ b/src/pages/api/scholarships.rs
@@ -1,10 +1,14 @@
#[cfg(feature = "ssr")]
-use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue};
+use aws_sdk_dynamodb::{
+ types::AttributeValue,
+ error::ProvideErrorMetadata,
+};
-use crate::common::ExpandableInfo;
use leptos::leptos_dom::log;
use leptos::prelude::ServerFnError;
use leptos::server;
+use uuid::Uuid;
+use crate::common::{ExpandableInfo, ValueType};
#[cfg(feature = "ssr")]
use crate::pages::utils::server_utils::create_dynamo_client;
@@ -28,8 +32,8 @@ pub async fn get_scholarship_info(id: String) -> Result {
@@ -46,10 +50,7 @@ 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()
@@ -75,6 +76,35 @@ pub async fn get_all_scholarship_info() -> Result, ServerFnE
.table_name("leptos-scholarship-test")
.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))
+ }
+ }
+}
+
+/// 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 {
@@ -89,3 +119,76 @@ pub async fn get_all_scholarship_info() -> Result, ServerFnE
}
}
}
+
+/// 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")));
+ }
+ }
+ }
+ }
+ }
+}
+
+/// 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> {
+ let client = create_dynamo_client().await;
+
+ 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")
+ .key("subject", AttributeValue::S(scholarship_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(())
+ }
+ Err(err) => {
+ let msg = err.message().unwrap_or("An unknown error occurred");
+ Err(ServerFnError::new(msg))
+ }
+ }
+}
diff --git a/src/pages/comparison_test.rs b/src/pages/comparison_test.rs
index 4ef31cb..8bb9ab8 100644
--- a/src/pages/comparison_test.rs
+++ b/src/pages/comparison_test.rs
@@ -1,25 +1,21 @@
use super::UnauthenticatedPage;
-use super::api::{get_all_scholarship_info, get_comparison_info};
use super::home_page::get_submission;
-use crate::common::{ComparisonData, ExpandableInfo, UserClaims, ValueType};
+use crate::common::{
+ ComparisonData, ExpandableInfo, ValueType,
+};
use crate::components::{ActionButton, Loading, Panel, Row};
use leptos::either::Either;
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 c3ca850..f4c6ed0 100644
--- a/src/pages/home_page.rs
+++ b/src/pages/home_page.rs
@@ -5,25 +5,26 @@ use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValu
#[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::pages::UnauthenticatedPage;
use crate::components::{
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
Select,
};
use crate::input;
-use crate::pages::UnauthenticatedPage;
-use base64::Engine;
use leptos::leptos_dom::logging::console_log;
use leptos::prelude::*;
+use leptos_oidc::{AuthLoaded, Authenticated};
+use base64::Engine;
use leptos::task::spawn_local;
-use leptos::wasm_bindgen::JsCast;
use leptos::web_sys::HtmlAnchorElement;
-use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
+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`.
@@ -144,15 +145,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/loaner_page.rs b/src/pages/loaner_page.rs
index b4acf04..b03cd9b 100644
--- a/src/pages/loaner_page.rs
+++ b/src/pages/loaner_page.rs
@@ -327,6 +327,8 @@ pub fn LoanerBorrowForm() -> impl IntoView {
#[component]
pub fn LoanerReturnForm() -> impl IntoView {
+ let navigate = use_navigate();
+
// Register server actions
let check_in_action = ServerAction::::new();
@@ -344,26 +346,18 @@ pub fn LoanerReturnForm() -> impl IntoView {
let dialog_ref = NodeRef::