Merge pull request #44 from R15-Software-Dev/feature/categorize-chips
A massive, unorganized PR.
This commit was merged in pull request #44.
This commit is contained in:
30 files changed
+1659
-920
No files matched your search
+10
-1
@@ -1,2 +1,11 @@
|
||||
[target.'cfg(target_os = "linux")']
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
rustflags = [
|
||||
"--cfg=web_sys_unstable_apis",
|
||||
"-C", "link-arg=-fuse-ld=mold"
|
||||
]
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
||||
Generated
+613
-753
File diff suppressed because it is too large.
Load diff
+3
-1
@@ -14,7 +14,7 @@ console_error_panic_hook = { version = "0.1", optional = true}
|
||||
leptos_axum = { version = "0.8", optional = true }
|
||||
leptos_meta = { version = "0.8" }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||
wasm-bindgen = { version = "=0.2.100", optional = true }
|
||||
wasm-bindgen = { version = "=0.2.106", optional = true }
|
||||
aws-sdk-dynamodb = { version = "1", optional = true }
|
||||
aws-config = { version = "1.6.2", optional = true }
|
||||
serde = "1.0.219"
|
||||
@@ -24,11 +24,13 @@ leptos_oidc = "0.9.0"
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.42"
|
||||
regex = "1.11.2"
|
||||
leptos_animate = "0.1.9"
|
||||
|
||||
macros = { path = "./macros" }
|
||||
traits = { path = "./traits" }
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
hydrate = [
|
||||
"leptos/hydrate",
|
||||
"dep:console_error_panic_hook",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
target
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "signup-lambda"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
lambda_runtime = "1.0.1"
|
||||
aws-sdk-cognitoidentityprovider = "1.105.0"
|
||||
serde = "1"
|
||||
tokio = { version = "1", features = ["macros"] }
|
||||
aws-config = "1.8.12"
|
||||
@@ -0,0 +1,52 @@
|
||||
# Introduction
|
||||
|
||||
signup-lambda is a Rust project that implements an AWS Lambda function in Rust.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Rust](https://www.rust-lang.org/tools/install)
|
||||
- [Cargo Lambda](https://www.cargo-lambda.info/guide/installation.html)
|
||||
|
||||
## Building
|
||||
|
||||
To build the project for production, run `cargo lambda build --release`. Remove the `--release` flag to build for development.
|
||||
|
||||
Read more about building your lambda function in [the Cargo Lambda documentation](https://www.cargo-lambda.info/commands/build.html).
|
||||
|
||||
## Testing
|
||||
|
||||
You can run regular Rust unit tests with `cargo test`.
|
||||
|
||||
If you want to run integration tests locally, you can use the `cargo lambda watch` and `cargo lambda invoke` commands to do it.
|
||||
|
||||
First, run `cargo lambda watch` to start a local server. When you make changes to the code, the server will automatically restart.
|
||||
|
||||
Second, you'll need a way to pass the event data to the lambda function.
|
||||
|
||||
You can use the existent [event payloads](https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/lambda-events/src/fixtures) in the Rust Runtime repository if your lambda function is using one of the supported event types.
|
||||
|
||||
You can use those examples directly with the `--data-example` flag, where the value is the name of the file in the [lambda-events](https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/lambda-events/src/fixtures) repository without the `example_` prefix and the `.json` extension.
|
||||
|
||||
```bash
|
||||
cargo lambda invoke --data-example apigw-request
|
||||
```
|
||||
|
||||
For generic events, where you define the event data structure, you can create a JSON file with the data you want to test with. For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "test"
|
||||
}
|
||||
```
|
||||
|
||||
Then, run `cargo lambda invoke --data-file ./data.json` to invoke the function with the data in `data.json`.
|
||||
|
||||
|
||||
Read more about running the local server in [the Cargo Lambda documentation for the `watch` command](https://www.cargo-lambda.info/commands/watch.html).
|
||||
Read more about invoking the function in [the Cargo Lambda documentation for the `invoke` command](https://www.cargo-lambda.info/commands/invoke.html).
|
||||
|
||||
## Deploying
|
||||
|
||||
To deploy the project, run `cargo lambda deploy`. This will create an IAM role and a Lambda function in your AWS account.
|
||||
|
||||
Read more about deploying your lambda function in [the Cargo Lambda documentation](https://www.cargo-lambda.info/commands/deploy.html).
|
||||
@@ -0,0 +1,76 @@
|
||||
use lambda_runtime::LambdaEvent;
|
||||
use aws_sdk_cognitoidentityprovider::Client;
|
||||
use aws_config::BehaviorVersion;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
use aws_sdk_cognitoidentityprovider::error::ProvideErrorMetadata;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct PostConfirmationEvent {
|
||||
version: String,
|
||||
trigger_source: String,
|
||||
region: String,
|
||||
user_pool_id: String,
|
||||
user_name: String,
|
||||
caller_context: CallerContext,
|
||||
request: Request,
|
||||
response: Response
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CallerContext {
|
||||
client_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Request {
|
||||
user_attributes: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Response {}
|
||||
|
||||
fn get_error_message(err: impl ProvideErrorMetadata) -> String {
|
||||
err.message().unwrap_or("An unknown error occurred").to_string()
|
||||
}
|
||||
|
||||
async fn add_user_to_group(provider: Client, user_id: String, group_name: impl Into<String>) -> Result<(), String> {
|
||||
let group_name = group_name.into();
|
||||
println!("Adding user {} to group {}", user_id, group_name);
|
||||
match provider
|
||||
.admin_add_user_to_group()
|
||||
.user_pool_id("us-east-1_Lfjuy5zaM")
|
||||
.username(user_id)
|
||||
.group_name(group_name)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(get_error_message(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handler(event: LambdaEvent<PostConfirmationEvent>) -> Result<PostConfirmationEvent, String> {
|
||||
println!("Found event: {:?}", event.payload);
|
||||
|
||||
let user_id = event.payload.request.user_attributes.get("sub").unwrap().to_string();
|
||||
let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
|
||||
let client = Client::new(&config);
|
||||
|
||||
// Check the client ID of the request to assign the user to the correct location.
|
||||
let res = match event.payload.caller_context.client_id.as_str() {
|
||||
"10jr2h3vtpu9n7gj46pvg5qo2q" => add_user_to_group(client, user_id, "ScholarshipStudents").await,
|
||||
"56c2bqvl021rv8d5mq36blt7jv" => add_user_to_group(client, user_id, "ScholarshipProviders").await,
|
||||
_ => Err("Invalid client ID, denying all user access.".to_string()),
|
||||
};
|
||||
|
||||
if res.is_ok() {
|
||||
Ok(event.payload)
|
||||
} else {
|
||||
Err(res.unwrap_err())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use lambda_runtime::{run, service_fn, tracing, Error};
|
||||
mod handler;
|
||||
|
||||
use handler::handler as function_handler;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
tracing::init_default_subscriber();
|
||||
|
||||
run(service_fn(function_handler)).await
|
||||
}
|
||||
+9
-8
@@ -1,9 +1,9 @@
|
||||
use crate::pages::{AboutPage, HomePage, LoanerPage, ProviderPortal, ComparisonTestPage, ScholarshipInfoPage, TestPage};
|
||||
use leptos::leptos_dom::log;
|
||||
use crate::pages::{AboutPage, HomePage, ProviderPortal, ComparisonTestPage, ScholarshipInfoPage, TestPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm};
|
||||
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, ParentRoute}, path};
|
||||
use crate::components::ToastList;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
@@ -65,15 +65,12 @@ pub fn AppWithRoutes() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
let current_origin = use_origin();
|
||||
log!("Current origin: {}", current_origin);
|
||||
|
||||
// Authentication setup
|
||||
let parameters = AuthParameters {
|
||||
issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM".into(),
|
||||
client_id: "10jr2h3vtpu9n7gj46pvg5qo2q".into(),
|
||||
redirect_uri: format!("{}/", use_origin()),
|
||||
// redirect_uri: Url::parse(format!("{}", current_origin).as_str()).unwrap().to_string(),
|
||||
// redirect_uri: Url::parse("http://localhost:3000").unwrap().to_string(),
|
||||
post_logout_redirect_uri: current_origin,
|
||||
scope: Some("openid%20profile%20email".into()),
|
||||
audience: None,
|
||||
@@ -98,6 +95,7 @@ pub fn AppWithRoutes() -> impl IntoView {
|
||||
<Title text="R15 Scholarship App DEV" />
|
||||
|
||||
<main>
|
||||
<ToastList>
|
||||
// TODO Create a 404 page
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=path!("") view=HomePage/>
|
||||
@@ -105,15 +103,18 @@ pub fn AppWithRoutes() -> impl IntoView {
|
||||
<Route path=path!("/test_page") view=TestPage/>
|
||||
<Route path=path!("/comparison") view=ComparisonTestPage />
|
||||
<Route path=path!("/providers") view=ProviderPortal/>
|
||||
<Route path=path!("/providers/profile") view=ProviderContactPage />
|
||||
<ParentRoute path=path!("/providers/scholarships") view=ScholarshipInfoPage>
|
||||
<Route path=path!(":id") view=ScholarshipInfoPage/>
|
||||
<Route path=path!("") view=ScholarshipInfoPage/>
|
||||
</ParentRoute>
|
||||
<ParentRoute path=path!("loaners") view=LoanerPage>
|
||||
<Route path=path!("") view=LoanerPage />
|
||||
<Route path=path!(":form_name") view=LoanerPage />
|
||||
<ParentRoute path=path!("loaners") view=LoanerShell>
|
||||
<Route path=path!("") view=LoanerFallback />
|
||||
<Route path=path!("borrowing") view=LoanerBorrowForm />
|
||||
<Route path=path!("returning") view=LoanerReturnForm />
|
||||
</ParentRoute>
|
||||
</Routes>
|
||||
</ToastList>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
use leptos_animate::animations::classes::{In, Out};
|
||||
|
||||
pub fn pop_in_out() -> (In, Out) {
|
||||
(
|
||||
In::default()
|
||||
.source("opacity-0 -translate-y-2 scale-90")
|
||||
.active("transition-all duration-150 transform-gpu")
|
||||
.target("opacity-100 translate-y-0 scale-100"),
|
||||
Out::default()
|
||||
.source("opacity-100 translate-y-0 scale-100")
|
||||
.active("transition-all duration-150 transform-gpu")
|
||||
.target("opacity-0 -translate-y-2 scale-90"),
|
||||
)
|
||||
}
|
||||
@@ -5,5 +5,6 @@ mod form_schema;
|
||||
mod user;
|
||||
mod value_type;
|
||||
mod submit_status;
|
||||
pub mod animations;
|
||||
|
||||
pub use self::{comparison::*, expandable_info::*, form_schema::*, user::*, value_type::*, form_params::*, submit_status::*};
|
||||
@@ -113,3 +113,31 @@ impl Display for ValueType {
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::types::AttributeValue;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl From<&AttributeValue> for ValueType {
|
||||
fn from(attr: &AttributeValue) -> Self {
|
||||
match attr {
|
||||
AttributeValue::S(s) => ValueType::String(Some(s.clone())),
|
||||
AttributeValue::N(n) => ValueType::Number(Some(n.clone())),
|
||||
AttributeValue::L(l) => ValueType::List(Some(l.iter().map(|v| v.into()).collect())),
|
||||
AttributeValue::M(m) => ValueType::Map(Some(m.iter().map(|(k, v)| (k.clone(), v.into())).collect())),
|
||||
_ => ValueType::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
impl Into<AttributeValue> for ValueType {
|
||||
fn into(self) -> AttributeValue {
|
||||
match self {
|
||||
ValueType::String(s) => AttributeValue::S(s.unwrap_or_default()),
|
||||
ValueType::Number(n) => AttributeValue::N(n.unwrap_or_default()),
|
||||
ValueType::List(l) => AttributeValue::L(l.unwrap_or_default().into_iter().map(|v| v.into()).collect()),
|
||||
ValueType::Map(m) => AttributeValue::M(m.unwrap_or_default().into_iter().map(|(k, v)| (k, v.into())).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ pub fn ActionButton(
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<button
|
||||
class="m-1.5 p-2.5 rounded-lg font-semibold text-white transition-all duration-400"
|
||||
class=(["bg-red-700", "hover:bg-red-800"], move || !disabled.get())
|
||||
class="m-1.5 p-2.5 rounded-lg font-semibold text-white transition-all duration-400 cursor-pointer"
|
||||
class=(["bg-red-800", "hover:bg-red-900"], move || !disabled.get())
|
||||
class=(["bg-gray-600", "pointer-events-none"], move || disabled.get())
|
||||
r#type=button_type
|
||||
disabled=move || disabled.get()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// # Header Component
|
||||
///
|
||||
/// A simple header that may also contain a description. Should be used to annotate sections and
|
||||
/// forms, with extra information about how that section/form will work.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```
|
||||
/// view! {
|
||||
/// <Header
|
||||
/// title: "Example header"
|
||||
/// description: "Example description"
|
||||
/// />
|
||||
/// }
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn Header(
|
||||
/// The title of the header. Must always be present.
|
||||
#[prop(into)] title: Signal<String>,
|
||||
/// The description of the header. Hidden when nothing i s
|
||||
#[prop(optional, into)] description: Signal<String>,
|
||||
) -> impl IntoView {
|
||||
let show_description = Signal::derive(move || {
|
||||
!description.get().is_empty()
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="m-1.5 mb-5">
|
||||
<div class="text-3xl font-bold">
|
||||
{title}
|
||||
</div>
|
||||
<Show when=move || show_description.get()>
|
||||
<div>
|
||||
{description}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -70,13 +70,22 @@ pub fn Chip(
|
||||
/// passed to the `on_change` prop for each individual `Chip` component.
|
||||
#[component]
|
||||
pub fn ChipsList(
|
||||
/// The member in the data map that this input affects.
|
||||
#[prop(into)] data_member: String,
|
||||
/// The data map that contains the list of values. This will be edited as the input changes.
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
/// The list of strings that should be displayed in the list.
|
||||
#[prop(into)] displayed_text: Signal<Vec<String>>,
|
||||
/// The list of values that should be displayed in the list.
|
||||
#[prop(into)] values: Signal<Vec<String>>,
|
||||
/// Indicates if the input should be disabled.
|
||||
#[prop(optional, into)] disabled: Signal<bool>,
|
||||
/// The label for the header of the input.
|
||||
#[prop(optional, into)] label: String,
|
||||
#[prop(optional, into)] required: Signal<bool>
|
||||
/// Indicates if this input is required.
|
||||
#[prop(optional, into)] required: Signal<bool>,
|
||||
/// Determines if this list behaves like a checkbox or radio list.
|
||||
#[prop(default = true.into(), into)] allows_multiple: Signal<bool>,
|
||||
) -> impl IntoView {
|
||||
let controller = use_selectable_list(
|
||||
data_member.clone(),
|
||||
@@ -88,7 +97,8 @@ pub fn ChipsList(
|
||||
<div class="flex flex-col">
|
||||
<div class="m-1.5 mt-0 mb-0">
|
||||
<span class="font-bold">{label}</span>
|
||||
{displayed_text.get().into_iter().zip(values.get())
|
||||
<div class="flex flex-row flex-wrap gap-2 mb-1">
|
||||
{move || displayed_text.get().into_iter().zip(values.get())
|
||||
.into_iter()
|
||||
.map(|(text, value)| {
|
||||
let value = RwSignal::new(value);
|
||||
@@ -100,14 +110,19 @@ pub fn ChipsList(
|
||||
let on_change = move || {
|
||||
// Update the data list by checking if the value is present in it. If so,
|
||||
// remove it. If not, add it.
|
||||
controller.selected_list.update(|list| {
|
||||
let value = value.get();
|
||||
let list = controller.selected_list.get();
|
||||
|
||||
if list.contains(&value) {
|
||||
list.retain(|val| *val != value);
|
||||
allows_multiple.get()
|
||||
.then(|| controller.selected_list.update(|list| list.retain(|v| v != &value)))
|
||||
.or_else(|| Some(controller.selected_list.set(Vec::new())));
|
||||
} else {
|
||||
list.push(value.clone());
|
||||
allows_multiple.get()
|
||||
.then(|| controller.selected_list.update(|list| list.push(value.clone())))
|
||||
.or_else(|| Some(controller.selected_list.set(vec![value.clone()])));
|
||||
}
|
||||
});
|
||||
|
||||
controller.dirty.set(true);
|
||||
};
|
||||
|
||||
@@ -122,7 +137,9 @@ pub fn ChipsList(
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect_view()}
|
||||
.collect_view()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Show when=move || controller.show_errors.get()>
|
||||
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
|
||||
|
||||
@@ -98,7 +98,7 @@ pub fn Select(
|
||||
value=""
|
||||
disabled
|
||||
hidden
|
||||
selected=raw_value.get().is_empty()
|
||||
selected={move || raw_value.get().is_empty()}
|
||||
>"Select one..."</option>
|
||||
{move || {
|
||||
value_list.iter().map(
|
||||
|
||||
@@ -9,9 +9,11 @@ mod text_field;
|
||||
mod utils;
|
||||
mod validated_form;
|
||||
mod lists;
|
||||
mod toasts;
|
||||
mod header;
|
||||
|
||||
pub use self::{
|
||||
action_button::*, banner::*, dashboard_button::*, lists::*,
|
||||
loading::*, multi_entry::*, panel::*, row::*, text_field::*,
|
||||
validated_form::*
|
||||
validated_form::*, toasts::*, header::*
|
||||
};
|
||||
@@ -57,8 +57,8 @@ pub fn OutlinedTextField(
|
||||
#[prop(optional, into)] placeholder: String,
|
||||
#[prop(into)] data_member: String, // should this be an RwSignal??
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop(optional)] input_type: TextFieldType,
|
||||
#[prop(into, optional)] disabled: Signal<bool>,
|
||||
#[prop(optional, into)] input_type: Signal<TextFieldType>,
|
||||
#[prop(optional, into)] disabled: Signal<bool>,
|
||||
#[prop(optional, into)] name: String,
|
||||
#[prop(optional, into)] label: String,
|
||||
#[prop(optional, into)] required: Signal<bool>
|
||||
@@ -69,13 +69,20 @@ pub fn OutlinedTextField(
|
||||
let validator_context = use_validation_context()
|
||||
.expect("FormValidSignal was not found");
|
||||
|
||||
let raw_value = RwSignal::new(data_map.get_untracked()
|
||||
let raw_value = Signal::derive({
|
||||
let data_member = data_member.clone();
|
||||
move ||
|
||||
data_map
|
||||
.get()
|
||||
.get(&data_member)
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.to_string());
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let dirty = RwSignal::new(false);
|
||||
let error = RwSignal::new(validate(required.get_untracked(), &raw_value.get_untracked(), &input_type));
|
||||
let error = Signal::derive(move ||
|
||||
validate(required.get(), &raw_value.get(), &input_type.get())
|
||||
);
|
||||
let show_errors = Signal::derive(move || {
|
||||
dirty.get() && matches!(error.get(), ValidationState::Invalid(_))
|
||||
});
|
||||
@@ -90,10 +97,10 @@ pub fn OutlinedTextField(
|
||||
});
|
||||
});
|
||||
|
||||
let str_type = match input_type {
|
||||
let str_type = Signal::derive(move || match input_type.get_untracked() {
|
||||
TextFieldType::Number => "number",
|
||||
_ => "text" // This is fine even for emails, as we're doing our own validation.
|
||||
};
|
||||
});
|
||||
|
||||
let on_blur = move |_| {
|
||||
// Check validity of input's current value. The value will be updated by on_input,
|
||||
@@ -107,9 +114,7 @@ pub fn OutlinedTextField(
|
||||
let data_member = data_member.clone();
|
||||
move |e| {
|
||||
let to_parse = event_target_value(&e);
|
||||
raw_value.set(to_parse.clone());
|
||||
error.set(validate(required.get(), &to_parse, &input_type));
|
||||
let into_map = match input_type {
|
||||
let into_map = match input_type.get() {
|
||||
TextFieldType::Number => ValueType::Number(Some(to_parse)),
|
||||
_ => ValueType::String(Some(to_parse))
|
||||
};
|
||||
@@ -137,11 +142,7 @@ pub fn OutlinedTextField(
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
prop:name=name
|
||||
prop:value=move || data_map
|
||||
.get() // HashMap
|
||||
.get(&data_member) // Option<ValueType>
|
||||
.unwrap_or(&ValueType::String(None)) // ValueType
|
||||
.to_string()
|
||||
prop:value=raw_value
|
||||
on:input=on_input
|
||||
on:blur=on_blur
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
mod toast_list;
|
||||
mod toast;
|
||||
|
||||
pub use toast_list::*;
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::time::Duration;
|
||||
use leptos::prelude::*;
|
||||
use leptos_animate::animate;
|
||||
use leptos_animate::animations::classes::{In, Out};
|
||||
|
||||
#[component]
|
||||
pub fn Toast(
|
||||
#[prop(into)] on_close: Callback<()>,
|
||||
#[prop(into)] message: Signal<String>,
|
||||
#[prop(into)] title: Signal<String>
|
||||
) -> impl IntoView {
|
||||
set_timeout(
|
||||
move || {
|
||||
on_close.run(())
|
||||
},
|
||||
Duration::from_secs(3)
|
||||
);
|
||||
|
||||
view! {
|
||||
<div
|
||||
class="flex flex-col rounded-md shadow-lg/33 bg-white p-3 m-1.5 mx-auto min-w-lg max-w-lg"
|
||||
use:animate=(
|
||||
In::default()
|
||||
.source("opacity-0 -translate-y-2 scale-90")
|
||||
.active("transition-all duration-150 transform-gpu")
|
||||
.target("opacity-100 translate-y-0 scale-100"),
|
||||
Out::default()
|
||||
.source("opacity-100 translate-y-0 scale-100")
|
||||
.active("transition-all duration-150 transform-gpu")
|
||||
.target("opacity-0 -translate-y-2 scale-90"),
|
||||
)
|
||||
>
|
||||
<h3 class="font-bold text-lg">{title}</h3>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::toasts::toast::Toast;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Toast {
|
||||
id: String,
|
||||
header: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl Toast {
|
||||
pub fn new() -> Self {
|
||||
Toast {
|
||||
id: "".to_owned(),
|
||||
header: "".to_owned(),
|
||||
message: "".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: impl Into<String>) -> Self {
|
||||
self.header = header.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn msg(mut self, msg: impl Into<String>) -> Self {
|
||||
self.message = msg.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn id(mut self, id: impl Into<String>) -> Self {
|
||||
self.id = id.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ToastContext {
|
||||
messages: RwSignal<Vec<Toast>>,
|
||||
}
|
||||
|
||||
impl ToastContext {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
messages: RwSignal::new(vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toast(&mut self, toast: Toast) {
|
||||
self.messages.update(|list| list.push(toast));
|
||||
}
|
||||
}
|
||||
|
||||
/// # Toast List Component
|
||||
/// Displays a list of toasts. They must be queued using the [`ToastContext`].
|
||||
#[component]
|
||||
pub fn ToastList(
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
// This context should be used wherever the application might need to use a toast.
|
||||
// That specific component should also be wrapped in a ToastList component, otherwise
|
||||
// this context won't exist.
|
||||
let context = use_context::<ToastContext>()
|
||||
.unwrap_or_else(provide_toast_context);
|
||||
|
||||
view! {
|
||||
{children()}
|
||||
<div class="fixed top-1 left-1/2 right-1/2 -translate-x-1/2 w-full">
|
||||
<For
|
||||
each=move || context.messages.get()
|
||||
key=|toast| toast.id.clone()
|
||||
let(Toast { id, header, message })
|
||||
>
|
||||
<Toast
|
||||
message=message
|
||||
title=header
|
||||
on_close=move || {
|
||||
context.messages.update(|list|
|
||||
list.retain(|v| v.id != id)
|
||||
);
|
||||
}
|
||||
/>
|
||||
</For>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn provide_toast_context() -> ToastContext {
|
||||
let context = ToastContext::new();
|
||||
provide_context(context);
|
||||
context
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos::web_sys::SubmitEvent;
|
||||
use crate::components::ActionButton;
|
||||
use crate::components::{ActionButton, Header};
|
||||
|
||||
/// Indicates the current state of an input.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
@@ -71,7 +71,11 @@ pub fn use_validation_context() -> Option<FormValidationRegistry> {
|
||||
/// Example usage in the view macro:
|
||||
/// ```
|
||||
/// view! {
|
||||
/// <ValidatedForm on_submit=Callback::new(move || {/* something here */})>
|
||||
/// <ValidatedForm
|
||||
/// on_submit=Callback::new(move || {/* something here */})
|
||||
/// title="Example Title"
|
||||
/// description="Example description."
|
||||
/// >
|
||||
/// {/* As many input components as you want */}
|
||||
/// </ValidatedForm>
|
||||
/// }
|
||||
@@ -102,6 +106,10 @@ pub fn ValidatedForm(
|
||||
children: Children,
|
||||
/// A callback that runs when the form successfully validates.
|
||||
#[prop(into)] on_submit: Callback<()>,
|
||||
/// The title of the included header.
|
||||
#[prop(into)] title: Signal<String>,
|
||||
/// The description for the included header.
|
||||
#[prop(optional, into)] description: Signal<String>,
|
||||
) -> impl IntoView {
|
||||
let validators = RwSignal::new(vec![]);
|
||||
|
||||
@@ -142,6 +150,10 @@ pub fn ValidatedForm(
|
||||
|
||||
view! {
|
||||
<form novalidate on:submit=submit_success_event>
|
||||
<Header
|
||||
title=title
|
||||
description=description
|
||||
/>
|
||||
{children()}
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
@@ -149,7 +161,7 @@ pub fn ValidatedForm(
|
||||
}
|
||||
button_type="submit"
|
||||
>
|
||||
"Submit form (but actually)"
|
||||
"Submit"
|
||||
</ActionButton>
|
||||
</form>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::pages::utils::server_utils::create_dynamo_client;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::logging::log;
|
||||
|
||||
use crate::common::{
|
||||
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
|
||||
TextComparison, TextListComparison, ValueType,
|
||||
@@ -186,6 +190,15 @@ fn create_sports_comparisons() -> Vec<ComparisonData> {
|
||||
|
||||
#[allow(unused)]
|
||||
fn make_comp_list() -> Vec<ComparisonData> {
|
||||
let gpa_3 = ComparisonData::new(
|
||||
"gpa_3",
|
||||
"weighted_gpa",
|
||||
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
|
||||
ValueType::Number(Some(3.0.to_string())),
|
||||
"GPA Limits",
|
||||
"GPA >= 3.0",
|
||||
);
|
||||
|
||||
let test_comp = ComparisonData::new(
|
||||
"math_sat_comp",
|
||||
"math_sat",
|
||||
@@ -195,20 +208,6 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
"Math SAT Score > 650",
|
||||
);
|
||||
|
||||
let service_hours_25 = ComparisonData::new(
|
||||
"map_sum_comp",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
ValueType::Number(Some(25.to_string())),
|
||||
"Community Involvement",
|
||||
"25 service hours",
|
||||
);
|
||||
|
||||
let service_hours_20 = ComparisonData::new(
|
||||
"service_hours_20",
|
||||
"community_involvement",
|
||||
@@ -219,8 +218,22 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
))),
|
||||
)),
|
||||
ValueType::Number(Some(20.to_string())),
|
||||
"Community Involvement",
|
||||
"20 service hours",
|
||||
"Community Service",
|
||||
"20+ service hours",
|
||||
);
|
||||
|
||||
let service_hours_25 = ComparisonData::new(
|
||||
"service_hours_25",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
ValueType::Number(Some(25.to_string())),
|
||||
"Community Service",
|
||||
"25+ service hours",
|
||||
);
|
||||
|
||||
let service_hours_30 = ComparisonData::new(
|
||||
@@ -233,8 +246,8 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
))),
|
||||
)),
|
||||
ValueType::Number(Some(30.to_string())),
|
||||
"Community Involvement",
|
||||
"30 service hours",
|
||||
"Community Service",
|
||||
"30+ service hours",
|
||||
);
|
||||
|
||||
let residency_southbury = ComparisonData::new(
|
||||
@@ -260,7 +273,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
"attended_bas",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Miscellaneous",
|
||||
"Additional Eligibility Factors",
|
||||
"Attended BAS",
|
||||
);
|
||||
|
||||
@@ -269,7 +282,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
"middsouth_church",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Miscellaneous",
|
||||
"Additional Eligibility Factors",
|
||||
"Member of Midd-South Church",
|
||||
);
|
||||
|
||||
@@ -278,7 +291,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
"family_military_service",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Miscellaneous",
|
||||
"Additional Eligibility Factors",
|
||||
"Family with Military Service",
|
||||
);
|
||||
|
||||
@@ -286,6 +299,7 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
let mut major_comps = Vec::<ComparisonData>::new();
|
||||
|
||||
let mut comp_list = vec![
|
||||
gpa_3,
|
||||
test_comp,
|
||||
service_hours_20,
|
||||
service_hours_25,
|
||||
@@ -305,8 +319,6 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
|
||||
#[server(CreateTestComparisons, endpoint = "/comparisons/create-test")]
|
||||
pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
use leptos::logging::log;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Creating test comparisons");
|
||||
@@ -331,8 +343,6 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
|
||||
#[server]
|
||||
pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError> {
|
||||
use leptos::logging::log;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Getting all comparisons from the database");
|
||||
@@ -359,3 +369,43 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_comparisons_categorized() -> Result<HashMap<String, Vec<ComparisonData>>, ServerFnError> {
|
||||
let client = create_dynamo_client().await;
|
||||
log!("Getting all comparisons from the database, by category.");
|
||||
|
||||
match client
|
||||
.scan()
|
||||
.table_name("leptos-comparison-test")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
if let Some(items) = output.items {
|
||||
log!("Found comparisons, categorizing.");
|
||||
|
||||
let items: Vec<ComparisonData> = serde_dynamo::from_items(items)?;
|
||||
|
||||
let categorized = items.iter().fold(
|
||||
HashMap::<String, Vec<ComparisonData>>::new(), |mut map, comp| {
|
||||
// Load each comparison into a map, then return that map.
|
||||
map.entry(comp.category.clone())
|
||||
.and_modify(|vec| vec.push(comp.clone()))
|
||||
.or_insert(vec![comp.clone()]);
|
||||
|
||||
map
|
||||
}
|
||||
);
|
||||
|
||||
Ok(categorized)
|
||||
} else {
|
||||
Err(ServerFnError::new("Couldn't find any comparisons."))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.message().unwrap_or("An unknown error occurred");
|
||||
Err(ServerFnError::new(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-2
@@ -8,7 +8,7 @@ use std::process::{Command, Stdio};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::io::Write;
|
||||
|
||||
use crate::common::ExpandableInfo;
|
||||
use crate::common::{ComparisonData, ExpandableInfo};
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
use crate::pages::utils::get_user_claims;
|
||||
use crate::components::{ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm};
|
||||
@@ -22,6 +22,7 @@ use leptos::task::spawn_local;
|
||||
use leptos::web_sys::HtmlAnchorElement;
|
||||
use leptos::wasm_bindgen::JsCast;
|
||||
use std::collections::HashMap;
|
||||
use crate::pages::api::get_comparisons_categorized;
|
||||
|
||||
/// # Get Student Info
|
||||
/// Gets a student's information given their `subject`.
|
||||
@@ -209,6 +210,28 @@ pub fn HomePage() -> impl IntoView {
|
||||
};
|
||||
//#endregion
|
||||
|
||||
let comparison_resource = Resource::new(
|
||||
move || user_subject.get(),
|
||||
async move |_| get_comparisons_categorized().await
|
||||
);
|
||||
|
||||
let comparison_info = RwSignal::new(HashMap::<String, Vec<ComparisonData>>::new());
|
||||
|
||||
Effect::new(move || {
|
||||
match comparison_resource.get() {
|
||||
Some(Ok(comparisons)) => {
|
||||
log!("Found comparison info: {:?}", comparisons);
|
||||
comparison_info.set(comparisons);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!("Error while getting comparisons: {:?}", err);
|
||||
}
|
||||
_ => {
|
||||
log!("An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
@@ -222,7 +245,11 @@ pub fn HomePage() -> impl IntoView {
|
||||
<Row>
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<ValidatedForm on_submit=Callback::new(on_submit)>
|
||||
<ValidatedForm
|
||||
on_submit=Callback::new(on_submit)
|
||||
title="Home Page Test Form"
|
||||
description="This really is just a test."
|
||||
>
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
"Region 15 Scholarship Application (DEMO)"
|
||||
|
||||
+67
-16
@@ -8,7 +8,7 @@ use serde_dynamo::{from_item, to_item};
|
||||
use crate::common::ValueType;
|
||||
|
||||
use crate::common::ExpandableInfo;
|
||||
use crate::components::{ActionButton, Banner, DashboardButton, OutlinedTextField, Panel, Row, Select, TextFieldType, ValidatedForm};
|
||||
use crate::components::{ActionButton, Banner, DashboardButton, OutlinedTextField, Panel, Row, Select, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use chrono::{FixedOffset, TimeZone};
|
||||
use leptos::Params;
|
||||
use leptos::either::{Either, EitherOf3};
|
||||
@@ -19,6 +19,13 @@ use leptos_router::hooks::{use_navigate, use_params};
|
||||
use leptos_router::params::Params;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use leptos_router::components::Outlet;
|
||||
use leptos_animate::animate;
|
||||
use crate::common::animations::pop_in_out;
|
||||
// Loaner page creates a single page that contains the borrowing/returning buttons
|
||||
// and a panel for a form. At first, it will show a fallback view. At the borrowing
|
||||
// path it will show the borrow form, and at the returning path it will show the
|
||||
// return form.
|
||||
|
||||
#[derive(Params, PartialEq)]
|
||||
struct LoanerParams {
|
||||
@@ -191,7 +198,7 @@ async fn create_borrow_entry(input: ExpandableInfo) -> Result<(), ServerFnError>
|
||||
/// create a true admin page for this, simply to be able to survey and override/remove information
|
||||
/// from the lists.
|
||||
#[component]
|
||||
pub fn LoanerPage() -> impl IntoView {
|
||||
pub fn LoanerShell() -> impl IntoView {
|
||||
// Check URL params. Depending on what string is visible here, we'll display a different form.
|
||||
let params = use_params::<LoanerParams>();
|
||||
let form_passed = move || {
|
||||
@@ -203,12 +210,6 @@ pub fn LoanerPage() -> impl IntoView {
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let form_view = move || match form_passed().as_str() {
|
||||
"borrowing" => EitherOf3::A(LoanerBorrowForm),
|
||||
"returning" => EitherOf3::B(LoanerReturnForm),
|
||||
_ => EitherOf3::C("Choose something from the left.".into_view()),
|
||||
};
|
||||
|
||||
// We'll create the page here - the sidebar contains all the buttons, while the form
|
||||
// container either shows forms or shows placeholder text.
|
||||
view! {
|
||||
@@ -234,15 +235,36 @@ pub fn LoanerPage() -> impl IntoView {
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-2">
|
||||
<Panel>{form_view}</Panel>
|
||||
<Panel>
|
||||
<Outlet />
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoanerFallback() -> impl IntoView {
|
||||
view! {
|
||||
<div
|
||||
class="flex flex-col items-center justify-center"
|
||||
use:animate=pop_in_out()
|
||||
>
|
||||
<div class="text-3xl font-bold mb-2">
|
||||
"Chromebook Loaner Form"
|
||||
</div>
|
||||
<div class="text-lg">
|
||||
"Select an option from the left to begin."
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LoanerBorrowForm() -> impl IntoView {
|
||||
let navigate = use_navigate();
|
||||
|
||||
// Register server actions.
|
||||
let create_entry_action = ServerAction::<CreateBorrowEntry>::new();
|
||||
|
||||
@@ -258,12 +280,29 @@ pub fn LoanerBorrowForm() -> impl IntoView {
|
||||
});
|
||||
};
|
||||
|
||||
//#region Toast Logic
|
||||
let mut toast_context = expect_context::<ToastContext>();
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(_)) = create_entry_action.value().get() {
|
||||
create_entry_action.clear();
|
||||
toast_context.toast(
|
||||
Toast::new()
|
||||
.id(uuid::Uuid::new_v4().to_string())
|
||||
.header("Borrower Added!")
|
||||
.msg("You can take your loaner now.")
|
||||
);
|
||||
navigate("/loaners", Default::default());
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
view! {
|
||||
<ValidatedForm on_submit=Callback::new(on_submit)>
|
||||
<div class="flex flex-col justify-center gap-2 mb-1 py-3 px-2">
|
||||
<h2 class="text-2xl font-bold">"Borrower Information"</h2>
|
||||
<p class="text-lg">"Please fill out the form below."</p>
|
||||
</div>
|
||||
<ValidatedForm
|
||||
use:animate=pop_in_out()
|
||||
on_submit=Callback::new(on_submit)
|
||||
title="Borrower Information"
|
||||
description="Please fill out the form below."
|
||||
>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
@@ -352,12 +391,19 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
|
||||
// Create an effect to run after check-in - refreshes the list with another scan request.
|
||||
// This could be beneficial if we ever decided to make multiple loaner kiosks (think SAT days)
|
||||
let mut toasts = expect_context::<ToastContext>();
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(_)) = check_in_action.value().get() {
|
||||
// Return to the main page.
|
||||
if let Some(dialog) = dialog_ref.get() {
|
||||
dialog.close();
|
||||
}
|
||||
toasts.toast(
|
||||
Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.header("Check in successful!")
|
||||
.msg("Don't forget to take your collateral.")
|
||||
);
|
||||
navigate("/loaners", Default::default());
|
||||
check_in_action.clear();
|
||||
}
|
||||
@@ -383,6 +429,7 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<dialog
|
||||
use:animate=pop_in_out()
|
||||
class="p-3 m-auto rounded-md shadow-lg/33 backdrop:backdrop-blur-xs backdrop:transition-backdrop-filter"
|
||||
node_ref=dialog_ref>
|
||||
<h2 class="text-2xl font-bold">"Confirmation"</h2>
|
||||
@@ -398,12 +445,16 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
<ActionButton on:click=close_dialog disabled=dialog_disabled>"Close"</ActionButton>
|
||||
<ActionButton on:click=dialog_check_in disabled=dialog_disabled>"Check In"</ActionButton>
|
||||
</dialog>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-2 m-2"
|
||||
use:animate=pop_in_out()
|
||||
>
|
||||
<div class="flex flex-col justify-center gap-2 mb-3 py-3 px-2">
|
||||
<h2 class="text-2xl font-bold">"Student List"</h2>
|
||||
<p class="text-lg">"Please select your name from the list."</p>
|
||||
</div>
|
||||
<Suspense fallback=move || "Loading...".into_view()>
|
||||
<div class="flex flex-col gap-2 m-2">
|
||||
{move || {
|
||||
match return_list_resource.get() {
|
||||
Some(loaner_list) => {
|
||||
@@ -445,7 +496,7 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
None => Either::Right("No loaners found.".into_view())
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -8,8 +8,9 @@ mod scholarship_info;
|
||||
mod test_page;
|
||||
mod unauthenticated_page;
|
||||
mod utils;
|
||||
mod provider_contact;
|
||||
|
||||
pub use self::{
|
||||
about_page::*, comparison_test::*, home_page::*, loaner_page::*, provider_portal::*,
|
||||
test_page::*, unauthenticated_page::*, scholarship_info::*
|
||||
test_page::*, unauthenticated_page::*, scholarship_info::*, provider_contact::*
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use crate::common::ValueType;
|
||||
use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
use crate::pages::utils::get_user_claims;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::pages::utils::server_utils::create_dynamo_client;
|
||||
|
||||
#[server]
|
||||
async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
|
||||
use aws_sdk_dynamodb::types::AttributeValue;
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Getting contact info for provider {}", id);
|
||||
|
||||
match client
|
||||
.get_item()
|
||||
.table_name("leptos-provider-contacts")
|
||||
.key("subject", AttributeValue::S(id))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
let Some(item) = output.item else {
|
||||
return Ok(HashMap::new());
|
||||
};
|
||||
|
||||
let map = item.iter().map(|(key, val)| {
|
||||
let val_type = ValueType::from(val);
|
||||
|
||||
(key.clone(), val_type)
|
||||
}).collect::<HashMap<String, ValueType>>();
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
Err(err) => {
|
||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||
Err(ServerFnError::new(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> {
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
use crate::pages::utils::server_utils::into_attr_map;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
let mut item = into_attr_map(contact_info);
|
||||
item.insert("subject".to_owned(), aws_sdk_dynamodb::types::AttributeValue::S(id));
|
||||
|
||||
match client
|
||||
.put_item()
|
||||
.table_name("leptos-provider-contacts")
|
||||
.set_item(Some(item))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||
Err(ServerFnError::new(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ProviderContactPage() -> impl IntoView {
|
||||
// Get user claims
|
||||
let user_claims = get_user_claims();
|
||||
let user_id = Memo::new(move |_| {
|
||||
user_claims.get()
|
||||
.map(|info| info.claims.subject.clone())
|
||||
});
|
||||
|
||||
// Get contact information
|
||||
let contact_info = RwSignal::new(HashMap::new());
|
||||
let contact_resource = Resource::new(
|
||||
move || user_id.get(),
|
||||
async move |id| {
|
||||
let Some(id) = id else {
|
||||
return Ok(HashMap::new());
|
||||
};
|
||||
|
||||
get_provider_contact(id).await
|
||||
}
|
||||
);
|
||||
|
||||
Effect::new(move || {
|
||||
match contact_resource.get() {
|
||||
Some(Ok(map)) => contact_info.set(map),
|
||||
Some(Err(e)) => log!("Error getting contact info: {}", e),
|
||||
None => log!("Contact info resource is not ready yet."),
|
||||
}
|
||||
});
|
||||
|
||||
// Set up submit action
|
||||
let submit_action = ServerAction::<PutProviderContact>::new();
|
||||
|
||||
let on_submit = move || {
|
||||
// Submit the form by reading the contact_info.
|
||||
submit_action.dispatch(PutProviderContact {
|
||||
id: user_id.get().unwrap_or_default(),
|
||||
contact_info: contact_info.get()
|
||||
});
|
||||
};
|
||||
|
||||
let elements_disabled = Signal::derive(move || {
|
||||
submit_action.pending().get()
|
||||
});
|
||||
|
||||
let mut toasts_context = expect_context::<ToastContext>();
|
||||
Effect::new(move || {
|
||||
submit_action.value().get().is_some()
|
||||
.then(|| {
|
||||
toasts_context.toast(
|
||||
Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.header("Submission Successful")
|
||||
.msg("You can go back or continue editing your responses.")
|
||||
);
|
||||
|
||||
submit_action.clear();
|
||||
});
|
||||
});
|
||||
|
||||
// Display contact form.
|
||||
view! {
|
||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/" />
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<Suspense fallback=Loading>
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<ValidatedForm
|
||||
on_submit=on_submit
|
||||
title="Contact Form"
|
||||
description="Enter your contact information and click submit."
|
||||
>
|
||||
{move || {
|
||||
contact_resource
|
||||
.get()
|
||||
.map(|_| {
|
||||
view! {
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member="first_name"
|
||||
data_map=contact_info
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member="last_name"
|
||||
data_map=contact_info
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=contact_info
|
||||
input_type=TextFieldType::Email
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Phone Number:"
|
||||
placeholder="123-456-7890"
|
||||
disabled=elements_disabled
|
||||
data_member="phone_number"
|
||||
data_map=contact_info
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Street Address:"
|
||||
placeholder="123 Fake Street"
|
||||
disabled=elements_disabled
|
||||
data_member="address"
|
||||
data_map=contact_info
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Testing Number"
|
||||
placeholder="Just put a number"
|
||||
disabled=elements_disabled
|
||||
data_member="testing_number"
|
||||
data_map=contact_info
|
||||
input_type=TextFieldType::Number
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</ValidatedForm>
|
||||
</Panel>
|
||||
<div class="flex flex-col flex-1" />
|
||||
</div>
|
||||
</Suspense>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ pub fn ProviderPortal() -> impl IntoView {
|
||||
title="Profile"
|
||||
description="Edit user profile"
|
||||
icon="/Person_Black.png"
|
||||
path="/"
|
||||
path="/providers/profile"
|
||||
/>
|
||||
<DashboardButton
|
||||
title="Create Scholarship"
|
||||
|
||||
+192
-83
@@ -6,11 +6,10 @@ use leptos::prelude::*;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use leptos_router::hooks::{use_navigate, use_params};
|
||||
use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
||||
use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, ValidatedForm};
|
||||
use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use super::UnauthenticatedPage;
|
||||
use crate::common::ComparisonData;
|
||||
use crate::pages::utils::get_user_claims;
|
||||
use super::api::{get_comparison_info, get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship};
|
||||
use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized, CreateTestComparisons};
|
||||
|
||||
|
||||
/// # Scholarship Info Page
|
||||
@@ -41,19 +40,13 @@ 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! {
|
||||
<Banner
|
||||
title="R15 Scholarships"
|
||||
logo="/PHS_Stacked_Acronym.png"
|
||||
path="/"
|
||||
/>
|
||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/" />
|
||||
<ToastList>
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<div class="flex flex-row align-top items-start">
|
||||
<div class="flex flex-col flex-1" />
|
||||
<ScholarshipList
|
||||
refresh_token=list_refresh
|
||||
on_delete=on_delete
|
||||
/>
|
||||
<ScholarshipList refresh_token=list_refresh on_delete=on_delete />
|
||||
<Transition fallback=Loading>
|
||||
<ScholarshipForm
|
||||
scholarship_id=scholarship_id
|
||||
@@ -66,6 +59,7 @@ pub fn ScholarshipInfoPage() -> impl IntoView {
|
||||
</div>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
</ToastList>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,37 +176,41 @@ fn ScholarshipList(
|
||||
});
|
||||
|
||||
view! {
|
||||
<dialog
|
||||
node_ref=delete_dialog_ref
|
||||
class="m-auto p-2"
|
||||
>
|
||||
<dialog node_ref=delete_dialog_ref class="m-auto p-2">
|
||||
<h2 class="text-2xl font-bold">"Confirm Deletion"</h2>
|
||||
<p>
|
||||
"Are you sure you want to delete "
|
||||
{move || {
|
||||
pending_delete.get().map(|s| {
|
||||
s.data.get("name").unwrap_or(&ValueType::String(None))
|
||||
pending_delete
|
||||
.get()
|
||||
.map(|s| {
|
||||
s.data
|
||||
.get("name")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or("<no name found>".to_string())
|
||||
})
|
||||
}}
|
||||
"?"
|
||||
}} "?"
|
||||
</p>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
let subject = pending_delete.get().map(|s| s.subject.clone()).unwrap_or_default();
|
||||
let subject = pending_delete
|
||||
.get()
|
||||
.map(|s| s.subject.clone())
|
||||
.unwrap_or_default();
|
||||
log!("Deleting scholarship with subject {}", subject);
|
||||
delete_action.dispatch(DeleteProviderScholarship {
|
||||
delete_action
|
||||
.dispatch(DeleteProviderScholarship {
|
||||
scholarship_id: subject,
|
||||
provider_id: provider_id.get().unwrap_or_default()
|
||||
provider_id: provider_id.get().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
disabled=modal_disabled
|
||||
>"Confirm"</ActionButton>
|
||||
<ActionButton
|
||||
on:click=move |_| pending_delete.set(None)
|
||||
>"Cancel"</ActionButton>
|
||||
>
|
||||
"Confirm"
|
||||
</ActionButton>
|
||||
<ActionButton on:click=move |_| pending_delete.set(None)>"Cancel"</ActionButton>
|
||||
</dialog>
|
||||
<div class="w-75">
|
||||
<Panel>
|
||||
@@ -222,31 +220,42 @@ fn ScholarshipList(
|
||||
<Transition fallback=Loading>
|
||||
<div class="flex flex-col gap-2">
|
||||
{move || {
|
||||
scholarships.get().map(|result| {
|
||||
scholarships
|
||||
.get()
|
||||
.map(|result| {
|
||||
match result {
|
||||
Ok(list) => {
|
||||
let views = list.iter().map(|entry| {
|
||||
let views = list
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
view! {
|
||||
<ScholarshipListEntry scholarship=entry.clone()
|
||||
on_delete=Callback::new(on_item_delete)/>
|
||||
<ScholarshipListEntry
|
||||
scholarship=entry.clone()
|
||||
on_delete=Callback::new(on_item_delete)
|
||||
/>
|
||||
}
|
||||
});
|
||||
Either::Left(views.collect_view())
|
||||
}
|
||||
Err(err) => {
|
||||
Either::Right(view! {
|
||||
Either::Right(
|
||||
view! {
|
||||
<div>
|
||||
<p>"Failed to load scholarship list: "{err.to_string()}</p>
|
||||
</div>
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}).collect_view()
|
||||
.collect_view()
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
<div class="p-2 bg-red-800 rounded-md text-center text-white hover:bg-red-900 transition-all"
|
||||
on:click=create_on_click>
|
||||
<div
|
||||
class="p-2 bg-red-800 rounded-md text-center text-white hover:bg-red-900 transition-all cursor-pointer"
|
||||
on:click=create_on_click
|
||||
>
|
||||
"Create New"
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,13 +315,15 @@ fn ScholarshipListEntry(
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="p-2 flex-1 bg-red-800 border-r border-white rounded-bl-md text-white text-center
|
||||
<div
|
||||
class="p-2 flex-1 bg-red-800 border-r border-white rounded-bl-md text-white text-center
|
||||
hover:bg-red-900 cursor-pointer transition-all"
|
||||
on:click=edit_click
|
||||
>
|
||||
"Edit"
|
||||
</div>
|
||||
<div class="p-2 flex-1 bg-red-800 border-l border-white rounded-br-md text-white text-center
|
||||
<div
|
||||
class="p-2 flex-1 bg-red-800 border-l border-white rounded-br-md text-white text-center
|
||||
hover:bg-red-900 cursor-pointer transition-all"
|
||||
on:click=move |_| on_delete.run(scholarship.clone())
|
||||
>
|
||||
@@ -350,15 +361,6 @@ fn ScholarshipForm(
|
||||
/// submission fails, this function is not run.
|
||||
#[prop(into)] on_submit: Callback<()>
|
||||
) -> impl IntoView {
|
||||
// let result_msg = Signal::derive(move || {
|
||||
// match submit_status.get() {
|
||||
// SubmitStatus::Idle => "".into(),
|
||||
// SubmitStatus::Sending => "Sending submission...".into(),
|
||||
// SubmitStatus::Success => "Success".into(),
|
||||
// SubmitStatus::Error(msg) => format!("Error: {}", msg),
|
||||
// }
|
||||
// });
|
||||
|
||||
let submit_action = ServerAction::<CreateScholarshipInfo>::new();
|
||||
|
||||
let submit_status = RwSignal::new(SubmitStatus::Idle);
|
||||
@@ -383,17 +385,19 @@ fn ScholarshipForm(
|
||||
}
|
||||
});
|
||||
|
||||
let comparison_list: Resource<Vec<ComparisonData>> = Resource::new(
|
||||
move || scholarship_id.get(), // There's no input to this function.
|
||||
let comparison_lists = Resource::new(
|
||||
move || scholarship_id.get().is_some(),
|
||||
async move |_| {
|
||||
get_comparison_info().await
|
||||
.unwrap_or_else(|e| {
|
||||
log!("Failed to get comparison info: {:?}", e);
|
||||
Vec::new()
|
||||
})
|
||||
get_comparisons_categorized().await
|
||||
}
|
||||
);
|
||||
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(map)) = comparison_lists.get() {
|
||||
log!("Available categories: {:?}", map.keys());
|
||||
}
|
||||
});
|
||||
|
||||
let scholarship_info = Resource::new(
|
||||
move || scholarship_id.get(),
|
||||
async move |opt_id| {
|
||||
@@ -406,31 +410,66 @@ fn ScholarshipForm(
|
||||
);
|
||||
|
||||
let form_data = RwSignal::new(HashMap::new());
|
||||
let chips_data = RwSignal::new(HashMap::new());
|
||||
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(scholarship)) = scholarship_info.get() {
|
||||
// Get the default chips data.
|
||||
let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") {
|
||||
map.clone()
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
chips_data.set(chips_default);
|
||||
form_data.set(scholarship.data);
|
||||
}
|
||||
});
|
||||
|
||||
let comparison_ids = Memo::new(move |_| {
|
||||
comparison_list.get().map(|list| list.iter()
|
||||
.map(|comp| comp.clone().id)
|
||||
.collect::<Vec<String>>()
|
||||
).unwrap_or_default()
|
||||
});
|
||||
let get_comp_category = move |category: &str| {
|
||||
if let Some(Ok(map)) = comparison_lists.get() {
|
||||
map.get(category).unwrap_or(&Vec::new()).clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
|
||||
let comparison_text = Memo::new(move |_| {
|
||||
comparison_list.get().map(|list| list.iter()
|
||||
.map(|comp| comp.clone().display_text)
|
||||
let get_comp_ids = move |category: &str| {
|
||||
get_comp_category(category)
|
||||
.into_iter()
|
||||
.map(|comp| comp.id)
|
||||
.collect::<Vec<String>>()
|
||||
).unwrap_or_default()
|
||||
});
|
||||
};
|
||||
|
||||
let get_comp_text = move |category: &str| {
|
||||
get_comp_category(category)
|
||||
.into_iter()
|
||||
.map(|comp| comp.display_text)
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
|
||||
// Get comparison lists.
|
||||
let sports_ids = Signal::derive(move || get_comp_ids("Sports Participation"));
|
||||
let sports_text = Signal::derive(move || get_comp_text("Sports Participation"));
|
||||
|
||||
let misc_ids = Signal::derive(move || get_comp_ids("Additional Eligibility Factors"));
|
||||
let misc_text = Signal::derive(move || get_comp_text("Additional Eligibility Factors"));
|
||||
|
||||
let comm_ids = Signal::derive(move || get_comp_ids("Community Service"));
|
||||
let comm_text = Signal::derive(move || get_comp_text("Community Service"));
|
||||
|
||||
let res_ids = Signal::derive(move || get_comp_ids("Residency"));
|
||||
let res_text = Signal::derive(move || get_comp_text("Residency"));
|
||||
|
||||
let gpa_ids = Signal::derive(move || get_comp_ids("GPA Limits"));
|
||||
let gpa_text = Signal::derive(move || get_comp_text("GPA Limits"));
|
||||
|
||||
// We'll collect the scholarship's name, num_awards, amount_per_award, total_awards,
|
||||
// fafsa_required, award_to, transcript_required, recipient_selection, essay_requirement (and prompt)
|
||||
// award_night_remarks
|
||||
|
||||
let mut toasts = expect_context::<ToastContext>();
|
||||
|
||||
let on_submit = move |_| {
|
||||
let mut info = ExpandableInfo::new(scholarship_id.get().unwrap_or_default());
|
||||
info.data = form_data.get();
|
||||
@@ -441,20 +480,44 @@ fn ScholarshipForm(
|
||||
});
|
||||
};
|
||||
|
||||
Effect::new(move || {
|
||||
submit_action.value().get().is_some()
|
||||
.then(|| {
|
||||
log!("Displaying toasts!");
|
||||
toasts.toast(
|
||||
Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.msg("Succesfully submitted.")
|
||||
);
|
||||
|
||||
submit_action.clear();
|
||||
});
|
||||
});
|
||||
|
||||
let create_comps = ServerAction::<CreateTestComparisons>::new();
|
||||
|
||||
view! {
|
||||
<Panel>
|
||||
<Show
|
||||
when=move || scholarship_id.get().is_some()
|
||||
fallback=|| view! { <p>"Choose a scholarship from the right, or create a new one."</p> }
|
||||
fallback=|| {
|
||||
view! { <p>"Choose a scholarship from the right, or create a new one."</p> }
|
||||
}
|
||||
>
|
||||
<Suspense fallback=Loading>
|
||||
<ValidatedForm on_submit=Callback::new(on_submit)>
|
||||
<ValidatedForm
|
||||
on_submit=Callback::new(on_submit)
|
||||
title="Scholarship Info Form"
|
||||
description="Create or edit a scholarship. Input all information and click Submit."
|
||||
>
|
||||
{move || {
|
||||
scholarship_info.get()
|
||||
scholarship_info
|
||||
.get()
|
||||
.map(|res_scholarship| {
|
||||
match res_scholarship {
|
||||
Ok(_) => {
|
||||
Either::Left(view! {
|
||||
Either::Left(
|
||||
view! {
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
"Scholarship Info Form (test)"
|
||||
@@ -491,7 +554,7 @@ fn ScholarshipForm(
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Total amount of all awards:"
|
||||
label="Total amount of all award(s): (estimates are acceptable)"
|
||||
placeholder="Enter an amount..."
|
||||
disabled=elements_disabled
|
||||
data_member="total_awards"
|
||||
@@ -505,7 +568,7 @@ fn ScholarshipForm(
|
||||
data_map=form_data
|
||||
items=vec!["Yes".to_string(), "No".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Do you require student financial information?"
|
||||
label="Do you require student FAFSA information?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -539,31 +602,77 @@ fn ScholarshipForm(
|
||||
<RadioList
|
||||
data_member="essay_prompt"
|
||||
data_map=form_data
|
||||
items=vec!["Test 1", "Test 2", "Test 3"].iter().map(|s| s.to_string()).collect()
|
||||
items=vec!["Test 1", "Test 2", "Test 3"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
label="If so, select a prompt from the list below."
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Scholarship Requirements"
|
||||
data_member="requirements"
|
||||
data_map=form_data
|
||||
values=comparison_ids
|
||||
displayed_text=comparison_text
|
||||
label="GPA Requirements"
|
||||
data_member="gpa"
|
||||
data_map=chips_data
|
||||
values=gpa_ids
|
||||
displayed_text=gpa_text
|
||||
/>
|
||||
</Row>
|
||||
})
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Sports Participation"
|
||||
data_member="sports_participation"
|
||||
data_map=chips_data
|
||||
values=sports_ids
|
||||
displayed_text=sports_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Community Service"
|
||||
data_member="community_service"
|
||||
data_map=chips_data
|
||||
values=comm_ids
|
||||
displayed_text=comm_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Residency"
|
||||
data_member="residency"
|
||||
data_map=chips_data
|
||||
values=res_ids
|
||||
displayed_text=res_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Additional Eligibility Factors"
|
||||
data_member="misc"
|
||||
data_map=chips_data
|
||||
values=misc_ids
|
||||
displayed_text=misc_text
|
||||
/>
|
||||
</Row>
|
||||
},
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
Either::Right(view! {
|
||||
Either::Right(
|
||||
view! {
|
||||
<p>"Error while getting scholarship: "{err.to_string()}</p>
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}).collect_view()
|
||||
.collect_view()
|
||||
}}
|
||||
</ValidatedForm>
|
||||
<ActionButton on:click=move |_| {
|
||||
create_comps.dispatch(CreateTestComparisons {});
|
||||
}>"Create Comparisons"</ActionButton>
|
||||
</Suspense>
|
||||
</Show>
|
||||
</Panel>
|
||||
|
||||
@@ -9,7 +9,9 @@ use crate::common::UserClaims;
|
||||
/// dependencies cannot be compiled for WASM (nor should they be).
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod server_utils {
|
||||
use std::collections::HashMap;
|
||||
use aws_config::SdkConfig;
|
||||
use crate::common::ValueType;
|
||||
|
||||
/// Creates an [`SdkConfig`] struct for use with AWS SDK structs.
|
||||
pub async fn create_aws_config() -> SdkConfig {
|
||||
@@ -20,6 +22,10 @@ pub mod server_utils {
|
||||
pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client {
|
||||
aws_sdk_dynamodb::Client::new(&create_aws_config().await)
|
||||
}
|
||||
|
||||
pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> {
|
||||
map.into_iter().map(|(k, v)| (k, v.into())).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current user claims. This function should only be used in an area that has
|
||||
|
||||
Reference in new issue
Block a user