Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1abedae760 | ||
|
|
1f51357ca5 | ||
|
|
756b23c673 | ||
|
|
17ee7bdc4b | ||
|
|
637f143ac1 | ||
|
|
7d02c6505e | ||
|
|
f7d66ccd8e | ||
|
|
bbc19b88be | ||
|
|
d32261fe09 | ||
|
|
ffccaa6b04 | ||
|
|
97b8305068 | ||
|
|
4f61222f2d | ||
|
|
494f93084f | ||
|
|
da2cca3359 | ||
|
|
6a24c18d6f | ||
|
|
caec4fb017 | ||
|
|
1b8406e09c | ||
|
|
838a412376 | ||
|
|
fdc5374f17 | ||
|
|
fe5a107c7c | ||
|
|
8417713aa1 | ||
|
|
bb743cf114 | ||
|
|
38994088df | ||
|
|
9b3f94f3ad | ||
|
|
b11ad5ffff | ||
|
|
956c653e28 | ||
|
|
4bf0cdcb39 | ||
|
|
15b675a595 | ||
|
|
5a064bdd69 | ||
|
|
d7c4c6b707 | ||
|
|
54a0753599 | ||
|
|
06364236c5 | ||
|
|
0ca00664b0 | ||
|
|
7ba22e5d24 | ||
|
|
f0f22bb68b | ||
|
|
bb5dfa2563 | ||
|
|
f9d9e400d3 | ||
|
|
8df0d10166 | ||
|
|
75f77d5ddf | ||
|
|
f51e88396b | ||
|
|
c97074f163 | ||
|
|
b4f7ff6382 | ||
|
|
b21887f82b | ||
|
|
dfd1594395 | ||
|
|
3f1745a589 | ||
|
|
7ca78b607b | ||
|
|
51f1acc00b | ||
|
|
765322bcca | ||
|
|
3b9e2598b4 | ||
|
|
a95e9a1034 | ||
|
|
12a980f327 | ||
|
|
aa912673c0 | ||
|
|
2069beba8a | ||
|
|
9427810026 | ||
|
|
4a2a5ca9ff | ||
|
|
9e418dddf5 | ||
|
|
5e45cc89fc | ||
|
|
032e978a34 | ||
|
|
11012bb567 | ||
|
|
354875186f | ||
|
|
c0d3288e28 | ||
|
|
e508e0a916 | ||
|
|
444e5ffb72 | ||
|
|
67586d0424 | ||
|
|
cc83d3f015 | ||
|
|
bab6cdae28 | ||
|
|
116d02029c | ||
|
|
b6bc412f56 | ||
|
|
84f27e3c9e | ||
|
|
0b2dc1b0c1 | ||
|
|
081d4ca5ca | ||
|
|
b17c462abb | ||
|
|
0c00c59162 | ||
|
|
ce05be8c02 | ||
|
|
95b69a36a8 | ||
|
|
e24dcd0a90 | ||
|
|
9ebab0689a | ||
|
|
84212d2360 | ||
|
|
0b4aae4db3 | ||
|
|
c3dbe3e2ac | ||
|
|
87aa104055 | ||
|
|
6105fa3bd5 | ||
|
|
bde15de634 | ||
|
|
7d7c96e642 | ||
|
|
f15d151694 | ||
|
|
5ea7461a4d | ||
|
|
67d0f766f8 | ||
|
|
09de9fcf91 | ||
|
|
d42573bf65 | ||
|
|
4e8f4a3c5e | ||
|
|
9eacc9a618 | ||
|
|
6fdf7f0113 | ||
|
|
e143149dac | ||
|
|
26ee1cf7b4 | ||
|
|
73392d0f42 | ||
|
|
9f208fb297 | ||
|
|
ed0a9a0fff | ||
|
|
c4d5c061a7 | ||
|
|
f3e2a0cb83 | ||
|
|
7cf2d132ae | ||
|
|
6a9ce30834 | ||
|
|
b19b696490 | ||
|
|
1d58e99878 | ||
|
|
8af64cbf35 | ||
|
|
5f6e0edab2 | ||
|
|
22d2c96520 | ||
|
|
c1f0c0cb81 | ||
|
|
f401e994a4 | ||
|
|
bb9eb465f4 | ||
|
|
53fd3a56c4 | ||
|
|
24c20f4199 | ||
|
|
7cf2facb18 | ||
|
|
03f7adb74d | ||
|
|
9e7fa3afcc | ||
|
|
5e9e9bcf58 | ||
|
|
fab172c4d1 | ||
|
|
38a0482b8c | ||
|
|
ae320f9763 | ||
|
|
d43a6ed153 | ||
|
|
e3ea13d07d | ||
|
|
7abb1c369f | ||
|
|
5ad00e69af | ||
|
|
bbeb713fd6 | ||
|
|
4115d24bd8 | ||
|
|
3b1bf88567 | ||
|
|
d5659f6da7 | ||
|
|
7645ecc240 |
No files matched your search
Generated
+217
-1
@@ -1548,6 +1548,211 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "412c95f764f89aad1a8505aa4273d931b66d5a26d1e6609a37243784a9128ad9"
|
||||
dependencies = [
|
||||
"icondata_ai",
|
||||
"icondata_bi",
|
||||
"icondata_bs",
|
||||
"icondata_cg",
|
||||
"icondata_ch",
|
||||
"icondata_core",
|
||||
"icondata_fa",
|
||||
"icondata_fi",
|
||||
"icondata_hi",
|
||||
"icondata_im",
|
||||
"icondata_io",
|
||||
"icondata_lu",
|
||||
"icondata_mdi",
|
||||
"icondata_oc",
|
||||
"icondata_ri",
|
||||
"icondata_si",
|
||||
"icondata_tb",
|
||||
"icondata_ti",
|
||||
"icondata_vs",
|
||||
"icondata_wi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_ai"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "153b31389f611b00f7392c2f79b5bb8332e30a5eee5bffd66fc4fe767c1de59c"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_bi"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f178e5643b61ddf06b4b0c68e0fa5674cbd750be9414e5e02e2e3dd740d9924"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_bs"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a8a72809ffc53e8a61dd19079693dbc0478b7b27e09ca1c1e81c04e283f485"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_cg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db064a49229a38dd5bae614b194adc5e3ee5351f5c5d9e539cd159bf84a92539"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_ch"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "998079d1d0673fbb1bd3753ef714e5668fb5f595ffa9bc89eb5b01b6b4f75ca5"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c97be924215abd5e630d84e95a47c710138a6559b4c55039f4f33aa897fa859"
|
||||
|
||||
[[package]]
|
||||
name = "icondata_fa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0108c409b3c06be1124a377b827922ad6091cb9eb467d521ea1cb3b1c3d7a76"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_fi"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0656636c0e5c070e8501667f287e1751ea4bec9945eed3c66ac472511632b9b0"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_hi"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f8ba6ed6e155898d296fc5d4127eb651b667643368698dd02b1e9383eab3eef"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_im"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e723800b6a6067a40cf311a9135d739412323f03f88e826a20f1249933187aa5"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_io"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7886629becd205d6f035dc084b68d2b9cc5ba8b03714f2c533c921ed98c6525a"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_lu"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f25663a6cc5371e9e33c98bab870f4aa2fd68c88853f1467c8fccfaf112782b1"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_mdi"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9517acba09219ff7caebd797b2db68f09b3621c6bc848fe9894f2d30e4f6cdb"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_oc"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "088863ff0b1dba4a6e8b3fec797be6ed8503c2d44fd47107f7f6da3abcb6cc87"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_ri"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b1851b5662f59b888ba24595886a9e6b6f5c11151b3640ee1875810f7544865"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_si"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68943eec8a766d8e5213385610aa47bf0bec5ddb76fdcf819721a05fb5c8f6c5"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_tb"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f08d4ae577034728dc8856d3bdbced6a406aadfd7386e0e24e5efb7be98685"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_ti"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a26660305b27c2831c8e701434e55f6fb589c12bdd43e5ec1e2ae270dbeb185e"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_vs"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe2547385392ec350a5a7f80c463767b1d7f7a37cf1431cac2754918a4c1b6a9"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_wi"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "352f5a5df000eabb35894cec75048e97a46d738ebdaf1e060670b0cf473a2419"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
@@ -1902,6 +2107,16 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leptos_icons"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "274959df1a852fc431b03f27d9cd3129fb0f8cca9c8d383e4d668f62543cbec9"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
"leptos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leptos_integration_utils"
|
||||
version = "0.8.7"
|
||||
@@ -2810,14 +3025,15 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"console_error_panic_hook",
|
||||
"icondata",
|
||||
"leptos",
|
||||
"leptos_animate",
|
||||
"leptos_axum",
|
||||
"leptos_icons",
|
||||
"leptos_meta",
|
||||
"leptos_oidc",
|
||||
"leptos_router",
|
||||
"macros",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_dynamo",
|
||||
"tokio",
|
||||
|
||||
+2
-1
@@ -23,11 +23,12 @@ uuid = { version = "1.17.0", features = ["serde", "v4", "js"] }
|
||||
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" }
|
||||
icondata = { version = "0.7.0", features = ["font-awesome"] }
|
||||
leptos_icons = "0.7.1"
|
||||
|
||||
[features]
|
||||
default = ["ssr"]
|
||||
|
||||
+92
-61
@@ -1,8 +1,11 @@
|
||||
use crate::pages::{AboutPage, HomePage, ProviderPortal, ComparisonTestPage, ScholarshipInfoPage, TestPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm};
|
||||
use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, AdminShell, AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminUtilsPage};
|
||||
use crate::pages::student;
|
||||
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 leptos_oidc::AuthSignal;
|
||||
use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path, MatchNestedRoutes};
|
||||
use leptos_router::any_nested_route::IntoAnyNestedRoute;
|
||||
use crate::components::login::ProviderLoginContext;
|
||||
use crate::components::ToastList;
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
@@ -31,10 +34,10 @@ pub fn App() -> impl IntoView {
|
||||
view! {
|
||||
// injects a stylesheet into the document <head>
|
||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css"/>
|
||||
|
||||
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css" />
|
||||
|
||||
// sets the document title
|
||||
<Title text="R15 Scholarship App DEV"/>
|
||||
<Title text="R15 Scholarship App DEV" />
|
||||
|
||||
// content for this welcome page
|
||||
<Router>
|
||||
@@ -43,82 +46,110 @@ pub fn App() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
fn use_origin() -> String {
|
||||
#[cfg(target_arch = "wasm32")] {
|
||||
use leptos::web_sys;
|
||||
// Just get the current URL origin.
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.location()
|
||||
.origin()
|
||||
.unwrap_or("http://localhost:3000/".to_string())
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AppWithRoutes() -> impl IntoView {
|
||||
provide_meta_context();
|
||||
|
||||
let current_origin = use_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()),
|
||||
post_logout_redirect_uri: current_origin,
|
||||
scope: Some("openid%20profile%20email".into()),
|
||||
audience: None,
|
||||
challenge: Challenge::None,
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
let _ = Auth::init(parameters);
|
||||
|
||||
view! {
|
||||
// injects a stylesheet into the document <head>
|
||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css" />
|
||||
|
||||
// <AuthLoading><p>"Authentication is loading"</p></AuthLoading>
|
||||
// <AuthErrorContext><AuthErrorView/></AuthErrorContext>
|
||||
|
||||
// sets the document title
|
||||
<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/>
|
||||
<Route path=path!("/about") view=AboutPage/>
|
||||
<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=LoanerShell>
|
||||
<Route path=path!("") view=LoanerFallback />
|
||||
<Route path=path!("borrowing") view=LoanerBorrowForm />
|
||||
<Route path=path!("returning") view=LoanerReturnForm />
|
||||
</ParentRoute>
|
||||
// <Route path=path!("/testing") view=TestPage />
|
||||
<ProviderRoutes />
|
||||
<LoanerRoutes />
|
||||
<StudentRoutes />
|
||||
</Routes>
|
||||
</ToastList>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
#[component(transparent)]
|
||||
fn ProviderRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("/providers") view=ProviderLoginContext>
|
||||
<Route path=path!("") view=ProviderPortal />
|
||||
<Route path=path!("/applicants") view=ApplicantsPageFallback />
|
||||
<Route path=path!("/callback") view=AuthCallbackPage />
|
||||
<Route path=path!("/profile") view=ProviderContactPage />
|
||||
<ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage>
|
||||
<Route path=path!(":id") view=ScholarshipInfoPage />
|
||||
<Route path=path!("") view=ScholarshipInfoPage />
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
.into_any_nested_route()
|
||||
}
|
||||
|
||||
#[component(transparent)]
|
||||
fn LoanerRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("loaners") view=LoanerShell>
|
||||
<Route path=path!("") view=LoanerFallback />
|
||||
<Route path=path!("borrowing") view=LoanerBorrowForm />
|
||||
<Route path=path!("returning") view=LoanerReturnForm />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
.into_any_nested_route()
|
||||
}
|
||||
|
||||
#[component(transparent)]
|
||||
fn AdminRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("admin") view=AdminShell>
|
||||
<Route path=path!("home") view=AdminHomePage />
|
||||
<Route path=path!("providers") view=AdminProviderPage />
|
||||
<Route path=path!("scholarships") view=AdminScholarshipPage />
|
||||
<Route path=path!("utilities") view=AdminUtilsPage />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
.into_any_nested_route()
|
||||
}
|
||||
|
||||
#[component(transparent)]
|
||||
fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
view! {
|
||||
<ParentRoute path=path!("students") view=student::StudentShell>
|
||||
<Route path=path!("callback") view=AuthCallbackPage />
|
||||
<Route path=path!("home") view=student::StudentHomePage />
|
||||
<Route path=path!("demographics") view=student::StudentDemographicsPage />
|
||||
<Route path=path!("eligibility") view=student::StudentEligibilityPage />
|
||||
<Route path=path!("additional") view=student::AdditionalPage />
|
||||
<Route path=path!("additional/academics") view=student::StudentAcademicsPage />
|
||||
<Route path=path!("additional/athletics") view=student::StudentAthleticsPage />
|
||||
<Route
|
||||
path=path!("additional/extracurriculars")
|
||||
view=student::StudentExtracurricularsPage
|
||||
/>
|
||||
<Route
|
||||
path=path!("additional/work-experience")
|
||||
view=student::StudentWorkExperiencePage
|
||||
/>
|
||||
<Route
|
||||
path=path!("additional/university")
|
||||
view=student::StudentUniversityPage
|
||||
/>
|
||||
<Route
|
||||
path=path!("additional/family-info")
|
||||
view=student::StudentFamilyPage
|
||||
/>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
.into_any_nested_route()
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AuthErrorView() -> impl IntoView {
|
||||
let auth = use_context::<AuthSignal>().expect("AuthError: RwSignal<AuthStore> was not found");
|
||||
|
||||
@@ -2,7 +2,8 @@ use super::{
|
||||
Comparison, MapListComparison, NestedListComparison, NumberComparison, NumberListComparison,
|
||||
TextComparison, TextListComparison,
|
||||
};
|
||||
use crate::common::{ExpandableInfo, ValueType};
|
||||
use crate::common::ValueType;
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
@@ -42,15 +43,13 @@ impl ComparisonData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare(&self, student_data: &ExpandableInfo) -> Result<bool, String> {
|
||||
let value = student_data.data.get(&self.member).expect(
|
||||
format!(
|
||||
"Couldn't find member {:?}, current student data: {:?}",
|
||||
self.member, student_data
|
||||
pub fn compare(&self, student_data: &HashMap<String, ValueType>) -> Result<bool, String> {
|
||||
student_data.get(&self.member)
|
||||
.ok_or_else(||
|
||||
format!("Couldn't find member {:?}, current student data: {:?}", self.member, student_data)
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
self.comparison.evaluate(value, &self.target_value)
|
||||
.and_then(|value| self.comparison.evaluate(value, &self.target_value))
|
||||
.or_else(|err| Err(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +105,11 @@ mod tests {
|
||||
display_text: "First Name is Jane".to_string(),
|
||||
};
|
||||
|
||||
let mut result = text_comp_success.compare(&student_data);
|
||||
let mut result = text_comp_success.compare(&student_data.data);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
|
||||
result = text_comp_fail.compare(&student_data);
|
||||
result = text_comp_fail.compare(&student_data.data);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,16 @@ impl Comparison for NumberComparison {
|
||||
// The prerequisites for all number comparisons is that the target value is Some(number) and the
|
||||
// comparison member points to Some(number) in the student_info.
|
||||
let student_value = match comp_value {
|
||||
ValueType::Number(Some(num)) => num.parse::<i32>().unwrap(),
|
||||
ValueType::Number(Some(num)) => num.parse::<f32>().unwrap(),
|
||||
_ => {
|
||||
return Err("Student value is not a number".into());
|
||||
}
|
||||
};
|
||||
|
||||
leptos::logging::log!("Parsing value: {:?}", target_value);
|
||||
|
||||
let target = match target_value {
|
||||
ValueType::Number(Some(num)) => num.parse::<i32>().unwrap(),
|
||||
ValueType::Number(Some(num)) => num.parse::<f32>().unwrap(),
|
||||
_ => {
|
||||
return Err("Target value is not a number".into());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
use chrono::{DateTime, FixedOffset, Local};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DateInfo {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub date: DateRange,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl DateInfo {
|
||||
pub fn get_start_date(&self) -> DateTime<FixedOffset> {
|
||||
match self.date {
|
||||
DateRange::Single(date) | DateRange::Range(date, _) => date,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DateRange {
|
||||
Single(DateTime<FixedOffset>),
|
||||
Range(DateTime<FixedOffset>, DateTime<FixedOffset>),
|
||||
}
|
||||
|
||||
impl DateRange {
|
||||
pub fn get_status(&self) -> DateStatus {
|
||||
match self {
|
||||
DateRange::Single(_) => DateStatus::Blank,
|
||||
DateRange::Range(start_date, end_date) => {
|
||||
let local = Local::now();
|
||||
// Check the amount of time from now to the start date. Values that are negative
|
||||
// indicate that the start date has already passed.
|
||||
if dbg!(start_date.signed_duration_since(local).num_days()) > 0 {
|
||||
DateStatus::Upcoming
|
||||
} else {
|
||||
// Check the amount of time from now to the end date. The value will be positive if
|
||||
// the end date is in the future. We will be specific to the minute.
|
||||
let duration_until = dbg!(end_date).signed_duration_since(local);
|
||||
let num_days = dbg!(duration_until.num_days());
|
||||
let num_mins = dbg!(duration_until.num_minutes());
|
||||
|
||||
if num_mins <= 0 {
|
||||
DateStatus::Closed
|
||||
} else if num_days <= 5 {
|
||||
DateStatus::Deadline
|
||||
} else {
|
||||
DateStatus::Open
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub enum DateStatus {
|
||||
Closed,
|
||||
Deadline,
|
||||
Open,
|
||||
Upcoming,
|
||||
Blank,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -26,7 +26,14 @@ pub enum InputType {
|
||||
}
|
||||
|
||||
impl InputType {
|
||||
pub fn into_view(self, data_map: RwSignal<HashMap<String, ValueType>>) -> impl IntoView {
|
||||
/// Converts an `InputType` into an `impl IntoView`. A name must be specified in the case that
|
||||
/// the component is a checkbox/radio list, which requires a unique name to function properly.
|
||||
/// Many times, the name will be the same as the data member.
|
||||
pub fn into_view(
|
||||
self,
|
||||
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
name: String,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
{match self {
|
||||
InputType::Text(member, label, placeholder, required) =>
|
||||
@@ -56,6 +63,7 @@ impl InputType {
|
||||
view! {
|
||||
<CheckboxList
|
||||
label = label.clone()
|
||||
name = name.clone()
|
||||
items = options.clone()
|
||||
data_member = member.clone()
|
||||
data_map = data_map
|
||||
|
||||
+7
-2
@@ -1,10 +1,15 @@
|
||||
mod comparison;
|
||||
mod date_info;
|
||||
mod expandable_info;
|
||||
mod form_params;
|
||||
mod form_schema;
|
||||
mod submit_status;
|
||||
mod user;
|
||||
mod value_type;
|
||||
mod submit_status;
|
||||
pub mod animations;
|
||||
mod tab_info;
|
||||
|
||||
pub use self::{comparison::*, expandable_info::*, form_schema::*, user::*, value_type::*, form_params::*, submit_status::*};
|
||||
pub use self::{
|
||||
comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*,
|
||||
submit_status::*, user::*, value_type::*, tab_info::*,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TabInfo {
|
||||
pub text: String,
|
||||
pub path: String,
|
||||
pub sub_paths: Vec<SubTabInfo>,
|
||||
}
|
||||
|
||||
impl TabInfo {
|
||||
pub fn new(
|
||||
text: impl Into<String>,
|
||||
path: impl Into<String>,
|
||||
sub_paths: Option<Vec<SubTabInfo>>
|
||||
) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
path: path.into(),
|
||||
sub_paths: sub_paths.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubTabInfo {
|
||||
pub text: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl SubTabInfo {
|
||||
pub fn new(text: impl Into<String>, path: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
path: path.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ pub fn ActionButton(
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<button
|
||||
class="m-1.5 p-2.5 rounded-lg font-semibold text-white transition-all duration-400 cursor-pointer"
|
||||
class="m-1.5 p-2.5 min-w-20 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
|
||||
|
||||
+31
-15
@@ -1,5 +1,6 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_navigate;
|
||||
use leptos_icons::Icon;
|
||||
|
||||
/// # Banner Component
|
||||
///
|
||||
@@ -21,28 +22,43 @@ pub fn Banner(
|
||||
title: String,
|
||||
/// The file path or URL of a logo displayed at the top left of the banner.
|
||||
#[prop(into)]
|
||||
logo: String,
|
||||
logo: Signal<String>,
|
||||
/// The route that the navigates user to a page (home page).
|
||||
#[prop(into, default = String::from("/"))]
|
||||
path: String,
|
||||
#[prop(into, optional)]
|
||||
path: Signal<String>,
|
||||
) -> impl IntoView {
|
||||
let navigate = use_navigate();
|
||||
|
||||
let on_click = move |_| navigate(&path, Default::default());
|
||||
let show_back = Signal::derive(move || !path.get().is_empty());
|
||||
|
||||
let on_click = move |_| {
|
||||
let path = path.get();
|
||||
if !path.is_empty() {
|
||||
navigate(&path, Default::default());
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="mx-auto">
|
||||
<div class="bg-red-900 shadow-lg shadow-[inset_0_2px_4px_0_rgba(0,0,0,0.3),0_4px_6px_-1px_rgba(127,29,29,0.3),0_2px_4px_-1px_rgba(127,29,29,0.2)]">
|
||||
<div class="flex items-center gap-4 p-6">
|
||||
<div class="flex-shrink-0">
|
||||
<img src={logo.clone()} class="h-10 w-10 cursor-pointer" alt="icon" on:click=on_click/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-white text-lg font-bold">{title.clone()}</h2>
|
||||
</div>
|
||||
<div class="w-screen bg-red-900">
|
||||
<div class="flex flex-row items-center gap-4 p-6 pb-2">
|
||||
<div class="flex-shrink-0">
|
||||
<img src=logo class="h-10 w-10 cursor-pointer" alt="icon" on:click=on_click />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-white text-2xl font-bold">{title.clone()}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-4" class=("p-2", move || !show_back.get())>
|
||||
<Show when=move || show_back.get()>
|
||||
<a
|
||||
href=path
|
||||
class="flex flex-row items-center gap-2 m-2 ml-14 text-gray-200 hover:underline"
|
||||
>
|
||||
<Icon icon=icondata::FaArrowLeftSolid />
|
||||
<span>"Back to Dashboard"</span>
|
||||
</a>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
@@ -41,21 +41,18 @@ pub fn DashboardButton(
|
||||
<button
|
||||
type="button"
|
||||
class="dashboard-button flex items-start gap-3 rounded-lg bg-white border-grey-300 p-6
|
||||
hover:bg-gray-100 transition cursor-pointer w-full text-left
|
||||
shadow-[inset_0_0_6px_rgba(0,0,0,0.12)]"
|
||||
on:click=on_click>
|
||||
hover:bg-gray-100 transition cursor-pointer w-full text-left
|
||||
shadow-[inset_0_0_6px_rgba(0,0,0,0.12)]"
|
||||
on:click=on_click
|
||||
>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<img src={icon.clone()} class="h-8 w-8" alt="icon"/>
|
||||
<h3 class="font-semibold text-base">
|
||||
{title.clone()}
|
||||
</h3>
|
||||
<img src=icon.clone() class="h-8 w-8" alt="icon" />
|
||||
<h3 class="font-semibold text-base">{title.clone()}</h3>
|
||||
|
||||
<p class="text-sm text-gray-600 pt-6">
|
||||
{description.clone()}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 pt-6">{description.clone()}</p>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use crate::common::{DateInfo, DateRange, DateStatus};
|
||||
use leptos::prelude::*;
|
||||
|
||||
fn render_dates(dates: DateRange) -> impl IntoView {
|
||||
let date_text = match dates {
|
||||
DateRange::Single(date) => format!("{}", date.format("%A %B %d, %I:%M%p")),
|
||||
DateRange::Range(start_date, end_date) =>
|
||||
format!("{} - {}",
|
||||
start_date.format("%D %I:%M%p"),
|
||||
end_date.format("%D %I:%M%p")
|
||||
),
|
||||
};
|
||||
|
||||
view! { <span>{move || date_text.clone()}</span> }
|
||||
}
|
||||
|
||||
fn render_status(date: DateRange) -> impl IntoView {
|
||||
let status = date.get_status();
|
||||
|
||||
let status_text = match dbg!(status.clone()) {
|
||||
DateStatus::Blank => "",
|
||||
DateStatus::Upcoming => "Upcoming",
|
||||
DateStatus::Open => "Open",
|
||||
DateStatus::Deadline => "Deadline",
|
||||
DateStatus::Closed => "Closed",
|
||||
};
|
||||
|
||||
let background_class = match status {
|
||||
DateStatus::Blank => "hidden border-none",
|
||||
DateStatus::Upcoming => "bg-yellow-100 text-yellow-800 border-yellow-100",
|
||||
DateStatus::Open => "bg-green-100 text-green-800 border-green-100",
|
||||
DateStatus::Deadline => "bg-red-100 text-red-800 border-red-100",
|
||||
DateStatus::Closed => "bg-red-700 text-white border-red-700"
|
||||
};
|
||||
|
||||
let classes =
|
||||
format!("inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs
|
||||
font-medium w-fit whitespace-nowrap shrink-0 {}",
|
||||
background_class
|
||||
);
|
||||
|
||||
view! { <span class=classes>{move || status_text}</span> }
|
||||
}
|
||||
|
||||
/// # Date Component
|
||||
/// A single date, for use in a [`DateList`] component.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```
|
||||
/// let date_info = DateInfo {
|
||||
/// title: "Example Title".to_string(),
|
||||
/// date: DateRange::Single("January 1st, 1990".to_string()),
|
||||
/// description: "A description of the date.".to_string(),
|
||||
/// status: DateStatus::Upcoming
|
||||
/// }
|
||||
///
|
||||
/// view! {
|
||||
/// <Date
|
||||
/// info=date_info
|
||||
/// />
|
||||
/// }
|
||||
/// ```
|
||||
#[component]
|
||||
fn Date(
|
||||
/// Struct of title, date, and description
|
||||
#[prop(into)] info: Signal<DateInfo>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<div class="flex items-start gap-3 rounded-lg border-grey-300 p-3
|
||||
w-full text-left shadow-lg">
|
||||
// <div class="flex-shrink-0 mt-0.5">
|
||||
// <img src={icon} class="h-4 w-4" alt="icon"/>
|
||||
// </div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="font-bold text-md text-base">{info.get().title}</h3>
|
||||
<h3 class="text-md text-base mt-1">
|
||||
{move || render_dates(info.get().date)}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">{info.get().description}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ml-2">{move || render_status(info.get().date)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
///# Dates List Component
|
||||
/// Displays a list of important dates in a column, with badges that draw attention to dates that
|
||||
/// are more important.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```
|
||||
/// let dates = vec![
|
||||
/// DateInfo {
|
||||
/// title: "Example Title".to_string(),
|
||||
/// date: DateRange::Single("January 1st, 1990".to_string()),
|
||||
/// description: "A description of the date.".to_string(),
|
||||
/// status: DateStatus::Upcoming
|
||||
/// }
|
||||
/// ]
|
||||
///
|
||||
/// view! {
|
||||
/// <DateList dates=dates />
|
||||
/// }
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn DateList(
|
||||
#[prop(into)] dates: Signal<Vec<DateInfo>>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
{move || {
|
||||
dates
|
||||
.get()
|
||||
.iter()
|
||||
.map(|info| {
|
||||
view! { <Date info=info.clone() /> }
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
}.into_any()
|
||||
}
|
||||
@@ -26,7 +26,7 @@ pub fn Header(
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="m-1.5 mb-5">
|
||||
<div class="flex flex-col gap-2 m-1.5 mt-3 mb-5">
|
||||
<div class="text-3xl font-bold">
|
||||
{title}
|
||||
</div>
|
||||
|
||||
@@ -41,11 +41,11 @@ pub fn Checkbox(
|
||||
#[prop(into)] value: Signal<String>,
|
||||
/// The name of the checkbox. This is used to group checkboxes together, so every checkbox
|
||||
/// in a group should have the same name.
|
||||
#[prop(into)] name: String,
|
||||
#[prop(into)] name: Signal<String>,
|
||||
/// A [`Signal`] indicating whether the checkbox is disabled.
|
||||
#[prop(into)] disabled: Signal<bool>,
|
||||
) -> impl IntoView {
|
||||
let id = create_unique_id(&name, &value.get_untracked());
|
||||
let id = create_unique_id(&name.get(), &value.get_untracked());
|
||||
|
||||
view! {
|
||||
<label for=id class="flex items-center">
|
||||
@@ -103,8 +103,12 @@ pub fn Checkbox(
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn CheckboxList(
|
||||
/// The name that this list should use. This doesn't need to be specified all the time, but is
|
||||
/// for use in elements that may treat this list as a sub-input (components like the [`MultiEntry`]
|
||||
/// do this)
|
||||
#[prop(optional, into)] name: Signal<String>,
|
||||
/// The data member that this list should edit/display from the `data_map`.
|
||||
#[prop(into)] data_member: String,
|
||||
#[prop(into)] data_member: Signal<String>,
|
||||
/// A [`Signal`] allowing access to a data store.
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
/// The available choices for this list.
|
||||
@@ -134,32 +138,33 @@ pub fn CheckboxList(
|
||||
let cloned = item.clone();
|
||||
move || cloned.clone()
|
||||
});
|
||||
|
||||
let checked = Signal::derive(move || {
|
||||
controller.selected_list.get().contains(&item.get())
|
||||
});
|
||||
|
||||
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 item = item.get();
|
||||
if list.contains(&item) {
|
||||
list.retain(|val| *val != item);
|
||||
} else {
|
||||
list.push(item);
|
||||
}
|
||||
});
|
||||
controller
|
||||
.selected_list
|
||||
.update(|list| {
|
||||
let item = item.get();
|
||||
if list.contains(&item) {
|
||||
list.retain(|val| *val != item);
|
||||
} else {
|
||||
list.push(item);
|
||||
}
|
||||
});
|
||||
controller.dirty.set(true);
|
||||
};
|
||||
|
||||
// Update the data list by checking if the value is present in it. If so,
|
||||
// remove it. If not, add it.
|
||||
|
||||
view! {
|
||||
<Checkbox
|
||||
checked=checked
|
||||
on_change=on_change
|
||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||
value=item
|
||||
name=data_member.clone()
|
||||
name=move || format!("{}_{}", name.get(), data_member.get())
|
||||
disabled=disabled
|
||||
/>
|
||||
}
|
||||
@@ -171,7 +176,7 @@ pub fn CheckboxList(
|
||||
{move || {
|
||||
match controller.error.get() {
|
||||
ValidationState::Invalid(msg) => msg,
|
||||
_ => "There is no error - should not see this message.".to_string()
|
||||
_ => "There is no error - should not see this message.".to_string(),
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
|
||||
@@ -27,14 +27,14 @@ use super::utils::use_selectable_list;
|
||||
pub fn Chip(
|
||||
#[prop(into)] display_text: Signal<String>,
|
||||
#[prop(into)] value: Signal<String>,
|
||||
#[prop(into)] name: String,
|
||||
#[prop(into)] name: Signal<String>,
|
||||
#[prop(into)] on_change: Callback<(), ()>,
|
||||
#[prop()] checked: Signal<bool>,
|
||||
#[prop(optional, into)] disabled: Signal<bool>,
|
||||
) -> impl IntoView {
|
||||
// Generate a unique id - ensure that the value doesn't contain spaces.
|
||||
// Without this id, checkbox/radio inputs can interfere with each other.
|
||||
let id = create_unique_id(&name, &value.get_untracked());
|
||||
let id = create_unique_id(&name.get(), &value.get_untracked());
|
||||
|
||||
view! {
|
||||
<label for=id>
|
||||
@@ -45,16 +45,17 @@ pub fn Chip(
|
||||
id=id.clone()
|
||||
prop:checked=checked
|
||||
on:change=move |_| on_change.run(())
|
||||
disabled=disabled />
|
||||
disabled=disabled
|
||||
/>
|
||||
<div class="rounded-full p-2.5
|
||||
cursor-pointer text-base
|
||||
bg-transparent border-1 border-gray-300
|
||||
shadow-md/50 hover:shadow-md/70
|
||||
peer-checked:bg-red-700 peer-checked:border-red-700 peer-checked:text-white
|
||||
peer-disabled:peer-checked:bg-gray-700
|
||||
peer-disabled:peer-checked:border-gray-700
|
||||
peer-disabled:cursor-default
|
||||
transition-all duration-150">
|
||||
cursor-pointer text-base
|
||||
bg-transparent border-1 border-gray-300
|
||||
shadow-md/50 hover:shadow-md/70
|
||||
peer-checked:bg-red-700 peer-checked:border-red-700 peer-checked:text-white
|
||||
peer-disabled:peer-checked:bg-gray-700
|
||||
peer-disabled:peer-checked:border-gray-700
|
||||
peer-disabled:cursor-default
|
||||
transition-all duration-150">
|
||||
<span>{display_text.clone()}</span>
|
||||
</div>
|
||||
</label>
|
||||
@@ -71,7 +72,7 @@ pub fn Chip(
|
||||
#[component]
|
||||
pub fn ChipsList(
|
||||
/// The member in the data map that this input affects.
|
||||
#[prop(into)] data_member: String,
|
||||
#[prop(into)] data_member: Signal<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.
|
||||
@@ -95,50 +96,63 @@ pub fn ChipsList(
|
||||
|
||||
view! {
|
||||
<div class="flex flex-col">
|
||||
<div class="m-1.5 mt-0 mb-0">
|
||||
<div class="flex flex-col gap-2 m-1.5 mt-0 mb-0">
|
||||
<span class="font-bold">{label}</span>
|
||||
<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);
|
||||
{move || {
|
||||
displayed_text
|
||||
.get()
|
||||
.into_iter()
|
||||
.zip(values.get())
|
||||
.into_iter()
|
||||
.map(|(text, value)| {
|
||||
let value = RwSignal::new(value);
|
||||
let checked = Signal::derive(move || {
|
||||
controller.selected_list.get().contains(&value.get())
|
||||
});
|
||||
let on_change = move || {
|
||||
let value = value.get();
|
||||
let list = controller.selected_list.get();
|
||||
if list.contains(&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 {
|
||||
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);
|
||||
};
|
||||
|
||||
let checked = Signal::derive(move || {
|
||||
controller.selected_list.get().contains(&value.get())
|
||||
});
|
||||
|
||||
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.
|
||||
let value = value.get();
|
||||
let list = controller.selected_list.get();
|
||||
|
||||
if list.contains(&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 {
|
||||
allows_multiple.get()
|
||||
.then(|| controller.selected_list.update(|list| list.push(value.clone())))
|
||||
.or_else(|| Some(controller.selected_list.set(vec![value.clone()])));
|
||||
view! {
|
||||
<Chip
|
||||
checked=checked
|
||||
on_change=on_change
|
||||
value=value
|
||||
display_text=text
|
||||
name=data_member
|
||||
disabled=disabled
|
||||
/>
|
||||
}
|
||||
|
||||
controller.dirty.set(true);
|
||||
};
|
||||
|
||||
view! {
|
||||
<Chip
|
||||
checked=checked
|
||||
on_change=on_change
|
||||
value=value
|
||||
display_text=text
|
||||
name=data_member.clone()
|
||||
disabled=disabled
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<Show when=move || controller.show_errors.get()>
|
||||
@@ -146,7 +160,7 @@ pub fn ChipsList(
|
||||
{move || {
|
||||
match controller.error.get() {
|
||||
ValidationState::Invalid(msg) => msg,
|
||||
_ => "There is no error - should not see this message.".to_string()
|
||||
_ => "There is no error - should not see this message.".to_string(),
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod utils;
|
||||
mod checkbox;
|
||||
mod chips;
|
||||
mod select_nav_list;
|
||||
|
||||
pub use checkbox::*;
|
||||
pub use chips::*;
|
||||
pub use chips::*;
|
||||
pub use select_nav_list::*;
|
||||
@@ -0,0 +1,179 @@
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
use crate::components::utils;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NavListInfo {
|
||||
pub text: Vec<String>,
|
||||
pub id: String,
|
||||
pub slug: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NavListItem(
|
||||
#[prop(into)] name: Signal<String>,
|
||||
#[prop(into)] value: Signal<String>,
|
||||
#[prop(into)] text: Signal<Vec<String>>,
|
||||
#[prop(into)] slug: Signal<String>,
|
||||
#[prop(into)] selected: Signal<bool>,
|
||||
#[prop(into)] on_change: Callback<()>,
|
||||
) -> impl IntoView {
|
||||
let linked_info = Signal::derive(move ||
|
||||
text.get()
|
||||
.get(0)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
);
|
||||
let other_info = Signal::derive(move ||
|
||||
text.get()
|
||||
.iter()
|
||||
.cloned()
|
||||
.skip(1)
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
|
||||
view! {
|
||||
<div
|
||||
class="flex flex-row gap-2 p-1 rounded-lg"
|
||||
class=(["bg-yellow-100"], move || selected.get())
|
||||
>
|
||||
<NavListCheckbox
|
||||
name=name
|
||||
value=value
|
||||
on_change=move || on_change.run(())
|
||||
selected=selected
|
||||
/>
|
||||
<A
|
||||
href=move || slug.get()
|
||||
{..}
|
||||
class="flex flex-1 items-center underline hover:text-blue-400"
|
||||
>
|
||||
{linked_info}
|
||||
</A>
|
||||
<For each=move || other_info.get() key=|info| info.clone() let(info)>
|
||||
<p class="flex flex-1 items-center">{info}</p>
|
||||
</For>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn NavListCheckbox(
|
||||
#[prop(into)] selected: Signal<bool>,
|
||||
#[prop(into)] on_change: Callback<()>,
|
||||
#[prop(into)] name: Signal<String>,
|
||||
#[prop(into)] value: Signal<String>,
|
||||
) -> impl IntoView {
|
||||
let id = Memo::new(move |_| utils::create_unique_id(&name.get(), &value.get()));
|
||||
|
||||
view! {
|
||||
<label for=id class="flex items-center">
|
||||
<input
|
||||
class="hidden relative peer shrink-0"
|
||||
type="checkbox"
|
||||
name=name
|
||||
id=id
|
||||
prop:checked=selected
|
||||
on:change=move |_| on_change.run(())
|
||||
/>
|
||||
<div class="relative inline-block h-5 w-5 m-2 rounded-sm border border-gray-300 bg-white
|
||||
transition-bg duration-150 peer-checked:bg-red-700 peer-checked:border-red-700
|
||||
[&>span]:opacity-0 peer-checked:[&>span]:opacity-100
|
||||
peer-disabled:bg-gray-600/33 peer-disabled:border-gray-600
|
||||
peer-disabled:peer-checked:bg-gray-600">
|
||||
// This span is the check icon. It is centered within the div and has a border on the bottom
|
||||
// and right sides, which is then rotated to appear like a checkmark.
|
||||
<span class="absolute left-1/2 top-1/2 block h-3 w-1.5 -translate-x-1/2 -translate-y-1/2
|
||||
rotate-45 transform border-b-3 border-r-3
|
||||
border-white transition-opacity duration-150"></span>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
/// # Selectable Nav-List Component
|
||||
///
|
||||
/// This component takes a `Vec<NavListInfo>` to show a list. The list contains links that append
|
||||
/// the `NavListItem::slug` to the current path, and each item is selectable. The list of selected
|
||||
/// items is usable in this component's parent.
|
||||
#[component]
|
||||
pub fn SelectableNavList(
|
||||
#[prop(into)] selected: RwSignal<Vec<String>>,
|
||||
#[prop(into)] items: Signal<Vec<NavListInfo>>,
|
||||
#[prop(into)] headers: Signal<Vec<String>>,
|
||||
#[prop(into)] name: Signal<String>,
|
||||
) -> impl IntoView {
|
||||
let all_selected = RwSignal::new(false);
|
||||
let all_selected_change = move || {
|
||||
if all_selected.get() {
|
||||
log!("Unchecking all values.");
|
||||
all_selected.set(false);
|
||||
selected.set(Vec::new());
|
||||
} else {
|
||||
log!("Checking all values.");
|
||||
all_selected.set(true);
|
||||
selected.set(
|
||||
items
|
||||
.get()
|
||||
.iter()
|
||||
.map(|info| info.id.clone())
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="flex flex-1 flex-col m-3">
|
||||
// Header block - contains evenly spaced headers and the "select all" checkbox.
|
||||
// Include a checkbox here that's got the same style as the items. This maintains spacing.
|
||||
// All headers *must* be evenly spaced, otherwise the list starts getting confusing.
|
||||
<div class="flex flex-row bg-red-800 rounded-lg p-1 gap-2">
|
||||
<NavListCheckbox
|
||||
selected=all_selected
|
||||
name=name
|
||||
value="select-all"
|
||||
on_change=all_selected_change
|
||||
/>
|
||||
<For each=move || headers.get() key=|header| header.clone() let(header_text)>
|
||||
<div class="flex flex-1 text-white font-bold items-center">{header_text}</div>
|
||||
</For>
|
||||
</div>
|
||||
// Items block - contains constructed items. These are selectable.
|
||||
<For
|
||||
each=move || items.get()
|
||||
key=|item| item.id.clone()
|
||||
children=move |entry| {
|
||||
let id = Signal::derive(move || entry.id.clone());
|
||||
let text = Signal::derive(move || entry.text.clone());
|
||||
let selected_sig = Signal::derive(move || selected.get().contains(&id.get()));
|
||||
let on_change = move || {
|
||||
selected
|
||||
.update(|list| {
|
||||
let id = id.get();
|
||||
if list.contains(&id) {
|
||||
log!("Found value {}, removing.", id);
|
||||
list.retain(|s| s != &id);
|
||||
} else {
|
||||
log!("Adding value {} to the list.", id);
|
||||
list.push(id);
|
||||
}
|
||||
});
|
||||
all_selected.set(false);
|
||||
};
|
||||
|
||||
view! {
|
||||
<NavListItem
|
||||
name=name
|
||||
text=text
|
||||
value=id.clone()
|
||||
slug=entry.slug
|
||||
selected=selected_sig
|
||||
on_change=on_change
|
||||
/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -23,24 +23,37 @@ fn value_type_to_vec(value_type: &ValueType) -> Vec<String> {
|
||||
}
|
||||
|
||||
pub fn use_selectable_list(
|
||||
data_member: String,
|
||||
data_member: Signal<String>,
|
||||
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
required: Signal<bool>
|
||||
) -> ListController {
|
||||
//#region Central List Logic
|
||||
|
||||
// Get default values from the data map. This will only run once, when the input is created
|
||||
// and mounted to the DOM.
|
||||
let temp_map = data_map.get_untracked();
|
||||
let origin_value_type = temp_map.get(&data_member)
|
||||
.unwrap_or(&ValueType::List(None));
|
||||
let origin_list = value_type_to_vec(origin_value_type);
|
||||
|
||||
// "Raw" selected values - just strings, no ValueTyping
|
||||
let selected_list = RwSignal::new(origin_list);
|
||||
let selected_list = RwSignal::new(Vec::new());
|
||||
let hydrated = RwSignal::new(false);
|
||||
let dirty = RwSignal::new(false);
|
||||
|
||||
// Hydration effect - runs only once, to hydrate data.
|
||||
Effect::new(move || {
|
||||
if hydrated.get() {
|
||||
return;
|
||||
}
|
||||
|
||||
// React to data_map changes
|
||||
let map = data_map.get();
|
||||
let key = data_member.get();
|
||||
|
||||
let list = map
|
||||
.get(&key)
|
||||
.unwrap_or(&ValueType::List(None));
|
||||
|
||||
selected_list.set(value_type_to_vec(list));
|
||||
|
||||
hydrated.set(true);
|
||||
});
|
||||
|
||||
Effect::new({
|
||||
let data_member = data_member.clone();
|
||||
move || {
|
||||
// Convert selected strings into ValueTypes
|
||||
let typed_list = selected_list.with(|list|
|
||||
@@ -51,7 +64,7 @@ pub fn use_selectable_list(
|
||||
|
||||
// Update the map
|
||||
data_map.update(|map| {
|
||||
map.insert(data_member.clone(), ValueType::List(Some(typed_list)));
|
||||
map.insert(data_member.get(), ValueType::List(Some(typed_list)));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -69,17 +82,23 @@ pub fn use_selectable_list(
|
||||
ValidationState::Valid
|
||||
}
|
||||
});
|
||||
let dirty = RwSignal::new(false);
|
||||
|
||||
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
|
||||
|
||||
let validator = RwSignal::new(InputState::new(
|
||||
data_member.clone(),
|
||||
data_member.get(),
|
||||
error.clone(),
|
||||
dirty.clone()
|
||||
));
|
||||
|
||||
validation_context.validators.update(|list| list.push(validator.clone()));
|
||||
|
||||
on_cleanup(move || {
|
||||
validation_context.validators.update(|list| {
|
||||
list.retain(|v| *v.get_untracked().input_name != *validator.get_untracked().input_name)
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
ListController {
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::common::ValueType;
|
||||
use crate::components::utils::create_unique_id;
|
||||
use leptos::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use leptos::html::Input;
|
||||
use crate::components::{use_validation_context, InputState, ValidationState};
|
||||
|
||||
#[component]
|
||||
@@ -20,9 +21,7 @@ pub fn Radio(
|
||||
// Using the "peer" class links this input with the div "peer" at the same level.
|
||||
// By using the "peer-checked" selector this div can toggle states based on this
|
||||
// input's state (checked/unchecked)
|
||||
class="
|
||||
relative peer shrink-0
|
||||
hidden"
|
||||
class="relative peer shrink-0 hidden"
|
||||
type="radio"
|
||||
id=id.clone()
|
||||
name=name
|
||||
@@ -32,7 +31,7 @@ pub fn Radio(
|
||||
/>
|
||||
// This div displays the box for the checkbox, and applies some styling to the child
|
||||
// span element.
|
||||
<div class="relative inline-block h-5 w-5 m-1 mr-2 rounded-full border border-gray-300 bg-white
|
||||
<div class="relative inline-block h-5 w-5 m-1 mr-2 rounded-full border border-gray-400 bg-white
|
||||
transition-bg duration-200 peer-checked:bg-red-700 peer-checked:border-red-700
|
||||
[&>span]:opacity-0 peer-checked:[&>span]:opacity-100
|
||||
peer-disabled:bg-gray-600/33 peer-disabled:border-gray-600
|
||||
@@ -43,7 +42,94 @@ pub fn Radio(
|
||||
rotate-45 transform border-3 border-3 rounded-full bg-white
|
||||
border-white transition-opacity duration-150"></span>
|
||||
</div>
|
||||
<span>{value.clone()}</span>
|
||||
<span class="px-0.5 py-0.5 flex-1">{value.clone()}</span>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
/// # Ratio Other Component
|
||||
/// This is a simple "other" field that contains a text input component. It can be selected using the
|
||||
/// same logic as the [`Radio`] component, and also has a secondary `RwSignal` that contains the
|
||||
/// current value of the embedded text input.
|
||||
///
|
||||
/// Example usage: <still working on this>
|
||||
#[component]
|
||||
fn RadioOther(
|
||||
#[prop(into)] name: Signal<String>,
|
||||
#[prop(into)] checked: Signal<bool>,
|
||||
#[prop(into)] on_change: Callback<()>,
|
||||
#[prop(into)] disabled: Signal<bool>,
|
||||
#[prop(optional, into)] value: RwSignal<String>,
|
||||
#[prop(into)] dirty: RwSignal<bool>,
|
||||
#[prop(into)] placeholder: Signal<String>,
|
||||
) -> impl IntoView {
|
||||
let id = create_unique_id(&name.get_untracked(), &"other".to_string());
|
||||
let input_ref = NodeRef::<Input>::new();
|
||||
|
||||
let input_disabled = Signal::derive(move || {
|
||||
!checked.get() || disabled.get()
|
||||
});
|
||||
|
||||
Effect::new(move || {
|
||||
checked.get()
|
||||
.then(|| {
|
||||
if let Some(node) = input_ref.get() {
|
||||
node.focus().expect("Couldn't focus input element");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let on_change_text = move |ev| {
|
||||
let val = event_target_value(&ev);
|
||||
|
||||
value.set(val);
|
||||
|
||||
on_change.run(());
|
||||
};
|
||||
|
||||
let on_blur_text = move |_| {
|
||||
dirty.set(true);
|
||||
};
|
||||
|
||||
view! {
|
||||
<label for=id class="flex items_center">
|
||||
<input
|
||||
// Using the "peer" class links this input with the div "peer" at the same level.
|
||||
// By using the "peer-checked" selector this div can toggle states based on this
|
||||
// input's state (checked/unchecked)
|
||||
class="relative peer shrink-0 hidden"
|
||||
type="radio"
|
||||
id=id.clone()
|
||||
name=name
|
||||
on:change=move |_| on_change.run(())
|
||||
prop:checked=checked
|
||||
disabled=disabled
|
||||
/>
|
||||
// This div displays the box for the checkbox, and applies some styling to the child
|
||||
// span element.
|
||||
<div class="relative inline-block min-h-5 min-w-5 h-5 w-5 m-1 mr-2 rounded-full border border-gray-400 bg-white
|
||||
transition-bg duration-200 peer-checked:bg-red-700 peer-checked:border-red-700
|
||||
[&>span]:opacity-0 peer-checked:[&>span]:opacity-100
|
||||
peer-disabled:bg-gray-600/33 peer-disabled:border-gray-600
|
||||
peer-disabled:peer-checked:bg-gray-600">
|
||||
|
||||
// This span is the check icon. It is centered within the div and has a border on the bottom
|
||||
// and right sides, which is then rotated to appear like a checkmark.
|
||||
<span class="absolute left-1/2 top-1/2 block h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2
|
||||
rotate-45 transform border-3 border-3 rounded-full bg-white
|
||||
border-white transition-opacity duration-150"></span>
|
||||
</div>
|
||||
<input
|
||||
node_ref=input_ref
|
||||
class="border-b-2 mb-1.5 px-1.5 py-0.5 rounded-t-xs bg-transparent relative flex-1 transition-all duration-150
|
||||
border-red-700 disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
r#type="text"
|
||||
disabled=input_disabled
|
||||
placeholder=placeholder
|
||||
prop:value=value
|
||||
on:input=on_change_text
|
||||
on:blur=on_blur_text
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -64,24 +150,48 @@ pub fn RadioList(
|
||||
#[prop(optional, into)] disabled: Signal<bool>,
|
||||
#[prop(optional, into)] label: String,
|
||||
#[prop(optional, into)] required: Signal<bool>,
|
||||
#[prop(optional, into)] other_prompt: Signal<String>,
|
||||
#[prop(optional, into)] name: Signal<String>
|
||||
) -> impl IntoView {
|
||||
//#region Value Logic
|
||||
|
||||
// Store a single signal that is the currently selected value. We can only have one selected
|
||||
// value, so there's no list needed. The default selected value is whatever the map has selected
|
||||
// already, or nothing.
|
||||
let default_value = match data_map.get_untracked().get(&data_member.get_untracked()) {
|
||||
Some(ValueType::String(Some(value))) => value.clone(),
|
||||
_ => String::new()
|
||||
};
|
||||
|
||||
let selected_value = RwSignal::new(default_value);
|
||||
let hydrated = RwSignal::new(false);
|
||||
let selected_value = RwSignal::new(String::new());
|
||||
let other_checked = RwSignal::new(false);
|
||||
let other_value = RwSignal::new(String::new());
|
||||
|
||||
// When the selected value changes, update the data map at the correct location
|
||||
// Hydration effect that resolves data. Sets up all data properly.
|
||||
Effect::new({
|
||||
let items = items.clone();
|
||||
move || {
|
||||
if hydrated.get() {
|
||||
return;
|
||||
}
|
||||
|
||||
let default_value = match data_map.get_untracked().get(&data_member.get_untracked()) {
|
||||
Some(ValueType::String(Some(value))) => value.clone(),
|
||||
_ => String::new()
|
||||
};
|
||||
|
||||
if !items.contains(&default_value) && !default_value.is_empty() {
|
||||
other_checked.set(true);
|
||||
other_value.set(default_value.clone());
|
||||
}
|
||||
|
||||
selected_value.set(default_value);
|
||||
|
||||
hydrated.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
// When the other option is selected and the other value changes, update the selected value.
|
||||
Effect::new(move || {
|
||||
data_map.update(|map| {
|
||||
map.insert(data_member.get(), ValueType::String(Some(selected_value.get())));
|
||||
});
|
||||
other_checked.get()
|
||||
.then(|| selected_value.set(other_value.get()));
|
||||
});
|
||||
|
||||
//#endregion
|
||||
@@ -96,13 +206,27 @@ pub fn RadioList(
|
||||
dirty.get() && matches!(error.get(), ValidationState::Invalid(_))
|
||||
});
|
||||
|
||||
let input_state = InputState::new(
|
||||
let input_state = RwSignal::new(InputState::new(
|
||||
data_member.get_untracked(),
|
||||
error.clone(),
|
||||
dirty.clone()
|
||||
);
|
||||
));
|
||||
|
||||
validation_context.validators.update(|list| list.push(RwSignal::new(input_state)));
|
||||
validation_context.validators.update(|list| list.push(input_state));
|
||||
|
||||
on_cleanup(move || {
|
||||
validation_context.validators.update(|list| {
|
||||
list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name)
|
||||
});
|
||||
});
|
||||
|
||||
// When the selected value changes, update the data map at the correct location and validate.
|
||||
Effect::new(move || {
|
||||
error.set(validate(selected_value.get(), required.get()));
|
||||
data_map.update(|map| {
|
||||
map.insert(data_member.get(), ValueType::String(Some(selected_value.get())));
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region Render Logic
|
||||
@@ -115,18 +239,23 @@ pub fn RadioList(
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let item = RwSignal::new(item);
|
||||
|
||||
let checked_signal = Signal::derive(move || {
|
||||
selected_value.get() == item.get()
|
||||
selected_value.get() == item.get() && !other_checked.get()
|
||||
});
|
||||
|
||||
let on_change = move || {
|
||||
// on_change only runs when the radio button is checked, not when unchecked.
|
||||
// Change the selected value to the new one.
|
||||
other_checked.set(false);
|
||||
selected_value.set(item.get());
|
||||
error.set(validate(selected_value.get_untracked(), required.get_untracked()));
|
||||
error
|
||||
.set(
|
||||
validate(
|
||||
selected_value.get_untracked(),
|
||||
required.get_untracked(),
|
||||
),
|
||||
);
|
||||
dirty.set(true);
|
||||
};
|
||||
// on_change only runs when the radio button is checked, not when unchecked.
|
||||
// Change the selected value to the new one.
|
||||
|
||||
view! {
|
||||
<Radio
|
||||
@@ -134,19 +263,31 @@ pub fn RadioList(
|
||||
on_change=on_change
|
||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||
value=item
|
||||
name=data_member
|
||||
name=move || format!("{}_{}", name.get(), data_member.get())
|
||||
disabled=disabled
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()}
|
||||
|
||||
<Show when=move || !other_prompt.get().is_empty()>
|
||||
<RadioOther
|
||||
checked=other_checked
|
||||
name=data_member
|
||||
disabled=disabled
|
||||
value=other_value
|
||||
on_change=move |_| other_checked.set(true)
|
||||
placeholder=other_prompt
|
||||
dirty=dirty
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when=move || show_errors.get()>
|
||||
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
|
||||
{move || {
|
||||
match error.get() {
|
||||
ValidationState::Invalid(msg) => msg,
|
||||
_ => "There is no error - should not see this message.".to_string()
|
||||
_ => "There is no error - should not see this message.".to_string(),
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@ use leptos::ev::{Event, Targeted};
|
||||
use leptos::prelude::*;
|
||||
use leptos::web_sys::HtmlSelectElement;
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::{log, warn};
|
||||
use crate::components::{use_validation_context, InputState, ValidationState};
|
||||
|
||||
fn validate(required: bool, value: String) -> ValidationState {
|
||||
@@ -20,7 +19,7 @@ fn validate(required: bool, value: String) -> ValidationState {
|
||||
/// by the `value` prop of the component.
|
||||
#[component]
|
||||
pub fn Select(
|
||||
#[prop()] value_list: Vec<String>,
|
||||
#[prop(into)] value_list: Signal<Vec<String>>,
|
||||
#[prop(into)] data_member: Signal<String>,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop(optional, into)] label: String,
|
||||
@@ -28,53 +27,43 @@ pub fn Select(
|
||||
#[prop(optional, into)] required: Signal<bool>
|
||||
) -> impl IntoView {
|
||||
//#region Value Logic
|
||||
|
||||
let default_raw_value = data_map.get_untracked()
|
||||
.get(&data_member.get_untracked())
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.as_string().unwrap_or_default().unwrap_or_default();
|
||||
|
||||
let raw_value = RwSignal::new(default_raw_value);
|
||||
|
||||
Effect::new(move || {
|
||||
data_map.update(|map| {
|
||||
map.insert(data_member.get(), ValueType::String(Some(raw_value.get())));
|
||||
});
|
||||
|
||||
let value = Memo::new(move |_| {
|
||||
data_map.with(|map| {
|
||||
map.get(&data_member.get_untracked())
|
||||
.and_then(|v| v.as_string().ok().flatten())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region Validation Logic
|
||||
|
||||
let error = RwSignal::new(
|
||||
validate(required.get_untracked(), raw_value.get_untracked())
|
||||
);
|
||||
warn!("Current error state: {:?}", error.get_untracked());
|
||||
let error = Memo::new(move |_| validate(required.get(), value.get()));
|
||||
let dirty = RwSignal::new(false);
|
||||
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
|
||||
|
||||
let input_state = InputState::new(data_member.get(), error.clone(), dirty.clone());
|
||||
let input_state = RwSignal::new(InputState::new(data_member.get(), error.clone(), dirty.clone()));
|
||||
|
||||
let validation_context = use_validation_context()
|
||||
.expect("Could not find FormValidationRegistry context");
|
||||
|
||||
validation_context.validators.update(|list| list.push(RwSignal::new(input_state)));
|
||||
validation_context.validators.update(|list| list.push(input_state));
|
||||
|
||||
on_cleanup(move || {
|
||||
validation_context.validators.update(|list| {
|
||||
list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name)
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region Event Logic
|
||||
|
||||
|
||||
let on_change = move |e: Targeted<Event, HtmlSelectElement>| {
|
||||
let value = e.target().value();
|
||||
log!("Found value {}", value);
|
||||
raw_value.set(value.clone());
|
||||
error.set(validate(required.get(), raw_value.get()));
|
||||
dirty.set(true);
|
||||
data_map.update(|map| {
|
||||
if let Some(val) = map.get_mut(&data_member.get()) {
|
||||
*val = ValueType::String(Some(value.clone()));
|
||||
} else {
|
||||
map.insert(data_member.get(), ValueType::String(Some(value)));
|
||||
}
|
||||
map.insert(data_member.get(), ValueType::String(Some(value)));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -87,27 +76,24 @@ pub fn Select(
|
||||
<span class="ml-1.5 mb-0 font-bold">{label}</span>
|
||||
<select
|
||||
class="relative flex-1 border-2 m-1.5 mt-0 p-1.5 rounded-md
|
||||
transition-border duration-150
|
||||
border-red-700 bg-transparent
|
||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
transition-border duration-150
|
||||
border-red-700 bg-transparent
|
||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
prop:value=move || value.get()
|
||||
on:change:target=on_change
|
||||
disabled=disabled
|
||||
>
|
||||
// This closure handles the display of the options.
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
hidden
|
||||
selected={move || raw_value.get().is_empty()}
|
||||
>"Select one..."</option>
|
||||
{move || {
|
||||
value_list.iter().map(
|
||||
move |value| {
|
||||
let display = value.to_owned();
|
||||
view! { <option value={display}>{display.clone()}</option> }
|
||||
}
|
||||
).collect::<Vec<_>>()
|
||||
}}
|
||||
<option value="" disabled hidden selected=move || value.get().is_empty()>
|
||||
"Select one..."
|
||||
</option>
|
||||
|
||||
// We can reasonably assume that the options in the list will be distinct.
|
||||
<For
|
||||
each=move || value_list.get()
|
||||
key=|value| value.clone()
|
||||
children=move |val| view! { <option value=val>{val.clone()}</option> }
|
||||
/>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@@ -116,7 +102,7 @@ pub fn Select(
|
||||
{move || {
|
||||
match error.get() {
|
||||
ValidationState::Invalid(msg) => msg,
|
||||
_ => "There is no error - should not see this message.".to_string()
|
||||
_ => "There is no error - should not see this message.".to_string(),
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ use leptos::prelude::*;
|
||||
#[component]
|
||||
pub fn Loading() -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
<div class="size-20 mx-auto mt-20">
|
||||
<img src="/loading.gif" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::login::setup::provide_auth_context;
|
||||
|
||||
#[component]
|
||||
pub fn AdminLoginContext(
|
||||
children: Children
|
||||
) -> impl IntoView {
|
||||
provide_auth_context(
|
||||
crate::utils::use_origin(),
|
||||
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
|
||||
"56c2bqvl021rv8d5mq36blt7jv",
|
||||
"/providers/callback"
|
||||
);
|
||||
|
||||
view! { {children()} }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub(crate) mod setup;
|
||||
mod provider;
|
||||
pub use provider::*;
|
||||
mod student;
|
||||
pub use student::*;
|
||||
mod admin;
|
||||
pub use admin::*;
|
||||
@@ -0,0 +1,28 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::Outlet;
|
||||
use super::setup::provide_auth_context;
|
||||
|
||||
///# Provider Login Context Component
|
||||
///
|
||||
/// This component handles providing context to the provider login information.
|
||||
/// It creates the corresponding [`AuthSignal`], specific to the student login page and redirects.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```
|
||||
/// view! {
|
||||
/// <ProviderLoginContext>
|
||||
/// // The rest of the student application. Don't forget AuthLoaded and
|
||||
/// // Authenticated components!
|
||||
/// </ProviderLoginContext>
|
||||
/// }
|
||||
#[component]
|
||||
pub fn ProviderLoginContext() -> impl IntoView {
|
||||
provide_auth_context(
|
||||
crate::utils::use_origin(),
|
||||
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
|
||||
"56c2bqvl021rv8d5mq36blt7jv",
|
||||
"/providers/callback"
|
||||
);
|
||||
|
||||
view! { <Outlet /> }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{Auth, AuthParameters, Challenge};
|
||||
|
||||
pub fn provide_auth_context(
|
||||
origin: String,
|
||||
issuer: impl Into<String>,
|
||||
client_id: impl Into<String>,
|
||||
redirect_path: impl Into<String>,
|
||||
) {
|
||||
let params = AuthParameters {
|
||||
issuer: issuer.into(),
|
||||
client_id: client_id.into(),
|
||||
redirect_uri: format!("{}{}", origin, redirect_path.into()),
|
||||
post_logout_redirect_uri: origin,
|
||||
scope: Some("openid%20profile%20email".into()),
|
||||
audience: None,
|
||||
challenge: Challenge::None,
|
||||
};
|
||||
|
||||
let auth = Auth::signal();
|
||||
provide_context(auth);
|
||||
|
||||
let _ = Auth::init(params);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use leptos::prelude::*;
|
||||
use super::setup::provide_auth_context;
|
||||
|
||||
///# Student Login Context Component
|
||||
///
|
||||
/// This component handles providing context to the student login information.
|
||||
/// It creates the corresponding [`AuthSignal`], specific to the student login page and redirects.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```
|
||||
/// view! {
|
||||
/// <StudentLoginContext>
|
||||
/// // The rest of the student application. Don't forget AuthLoaded and
|
||||
/// // Authenticated components!
|
||||
/// </StudentLoginContext>
|
||||
/// }
|
||||
#[component]
|
||||
pub fn StudentLoginContext(
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
provide_auth_context(
|
||||
crate::utils::use_origin(),
|
||||
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
|
||||
"10jr2h3vtpu9n7gj46pvg5qo2q",
|
||||
"/students/callback"
|
||||
);
|
||||
|
||||
view! { {children()} }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod action_button;
|
||||
mod banner;
|
||||
mod dashboard_button;
|
||||
mod date;
|
||||
mod loading;
|
||||
mod multi_entry;
|
||||
mod panel;
|
||||
@@ -11,9 +12,11 @@ mod validated_form;
|
||||
mod lists;
|
||||
mod toasts;
|
||||
mod header;
|
||||
pub mod login;
|
||||
mod tabs;
|
||||
|
||||
pub use self::{
|
||||
action_button::*, banner::*, dashboard_button::*, lists::*,
|
||||
loading::*, multi_entry::*, panel::*, row::*, text_field::*,
|
||||
validated_form::*, toasts::*, header::*
|
||||
validated_form::*, toasts::*, header::*, date::*, tabs::*
|
||||
};
|
||||
+112
-66
@@ -1,8 +1,9 @@
|
||||
use crate::common::{InputType, ValueType};
|
||||
use crate::components::ActionButton;
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::log;
|
||||
use leptos_icons::Icon;
|
||||
use uuid::Uuid;
|
||||
|
||||
// region Helper Functions
|
||||
@@ -12,6 +13,14 @@ fn get_uuid_from_map(map: &HashMap<String, ValueType>) -> Option<String> {
|
||||
map.get("uuid")?.as_string().unwrap_or(None)
|
||||
}
|
||||
|
||||
/// Does the same as [`get_uuid_from_map`], but starts from a [`ValueType`] enum.
|
||||
fn get_uuid(vt: ValueType) -> Option<String> {
|
||||
vt.as_map().ok().flatten().unwrap_or_default()
|
||||
.get("uuid")?
|
||||
.as_string()
|
||||
.ok().flatten()
|
||||
}
|
||||
|
||||
/// Updates an Entry within the given `Vec`.
|
||||
fn update_entry_list(list: &mut Vec<ValueType>, data: &HashMap<String, ValueType>) {
|
||||
if let Some(uuid) = get_uuid_from_map(&data) {
|
||||
@@ -66,81 +75,103 @@ fn new_entry() -> ValueType {
|
||||
/// `first_name` and `last_name` keys within that `Entry`.
|
||||
#[component]
|
||||
pub fn MultiEntry(
|
||||
#[prop(into)] label: Signal<String>,
|
||||
#[prop(optional, into)] description: Signal<String>,
|
||||
/// The data member that contains the list of entries within the `data_map`.
|
||||
#[prop(into)]
|
||||
data_member: String,
|
||||
data_member: Signal<String>,
|
||||
/// The data map that contains all the information. Data is found within this map using the
|
||||
/// component's `data_member` input.
|
||||
#[prop()]
|
||||
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
/// The type and order of the inputs for each `Entry` component within this `MultiEntry` component.
|
||||
#[prop(optional)]
|
||||
schema: Vec<InputType>,
|
||||
#[prop(optional, into)]
|
||||
schema: Signal<Vec<InputType>>,
|
||||
) -> impl IntoView {
|
||||
// Make the values within the list reactive. Each of these values should be a ValueType::Map,
|
||||
// so we can pass these directly to the application.
|
||||
let data_reactive = RwSignal::new(
|
||||
data_map
|
||||
.get_untracked()
|
||||
.get(&data_member)
|
||||
.unwrap_or(&ValueType::List(None))
|
||||
.as_list()
|
||||
.unwrap_or(Some(Vec::<ValueType>::new()))
|
||||
.unwrap_or(Vec::new()),
|
||||
);
|
||||
|
||||
// This effect updates the data_map with any new values from the data_reactive list, every time
|
||||
// the data_reactive list is changed. This allows changes from within the Entry component to
|
||||
// bubble up to the data_map effectively.
|
||||
Effect::new(move |_| {
|
||||
let entry_list = data_reactive.get();
|
||||
data_map.update(|map| {
|
||||
map.insert(data_member.clone(), ValueType::List(Some(entry_list)));
|
||||
});
|
||||
let data_list = Memo::new(move |_| {
|
||||
data_map.with(|map| {
|
||||
map.get(&data_member.get())
|
||||
.and_then(|v| v.as_list().ok().flatten())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
});
|
||||
|
||||
// This effect updates the data_map with any new values from the data_list, every time
|
||||
// the data_list is changed. This allows changes from within the Entry component to
|
||||
// bubble up to the data_map effectively.
|
||||
let update_parent_list = move |list| {
|
||||
log!("Updating parent list using child list {:?}", list);
|
||||
data_map.update(|map| {
|
||||
map.insert(data_member.get(), ValueType::List(Some(list)));
|
||||
});
|
||||
};
|
||||
|
||||
let add_entry = move |_| data_reactive.update(|list| list.push(new_entry()));
|
||||
let add_entry = move |_| data_map.update(|map| {
|
||||
log!("Adding new entry.");
|
||||
let mut list = data_list.get_untracked();
|
||||
|
||||
list.push(new_entry());
|
||||
map.insert(data_member.get(), ValueType::List(Some(list)));
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col gap-2 p-2">
|
||||
<span class="font-bold">{label}</span>
|
||||
<Show when=move || !description.get().is_empty()>
|
||||
<span>{description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6 p-2">
|
||||
// Render the entries
|
||||
<For
|
||||
each=move || data_reactive.get()
|
||||
key=|entry| get_uuid_from_map(&entry.as_map().unwrap().unwrap()) // Entries MUST be given a unique ID. This is absolutely ridiculous.
|
||||
children=move |mut entry_map| {
|
||||
// The map_signal is the map used for the internal element. This signal will
|
||||
// keep track of the individual entry's data, and whenever it is changed,
|
||||
// the parent data_map will also be updated to reflect these changes.
|
||||
let map_signal = RwSignal::new(entry_map
|
||||
.as_map()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default());
|
||||
|
||||
// This effect updates the data_reactive list with the map_signal values, every
|
||||
// time the map_signal is changed.
|
||||
Effect::new(move |_| {
|
||||
let child_map = map_signal.get(); // HashMap
|
||||
data_reactive.update(|mut list| {
|
||||
update_entry_list(&mut list, &child_map);
|
||||
});
|
||||
});
|
||||
|
||||
view! {
|
||||
<Entry
|
||||
data_map = map_signal
|
||||
schema = schema.clone()
|
||||
/>
|
||||
}
|
||||
<Show
|
||||
when=move || !data_list.get().is_empty()
|
||||
fallback=|| {
|
||||
view! { <div class="mx-auto">"You haven't added any entries yet."</div> }
|
||||
}
|
||||
/>
|
||||
>
|
||||
<For
|
||||
each=move || data_list.get()
|
||||
// Entries MUST be given a unique ID.
|
||||
key=|entry| get_uuid(entry.clone())
|
||||
children=move |mut entry_map| {
|
||||
let map_signal = RwSignal::new(
|
||||
entry_map.as_map().ok().flatten().unwrap_or_default(),
|
||||
);
|
||||
let unique_id = Memo::new(move |_| {
|
||||
get_uuid_from_map(&map_signal.get()).unwrap_or_default()
|
||||
});
|
||||
let on_delete = move || {
|
||||
let mut list = data_list.get_untracked();
|
||||
let uuid = unique_id.get_untracked();
|
||||
list.retain(|map| {
|
||||
get_uuid(map.clone()).unwrap_or_default() != uuid
|
||||
});
|
||||
update_parent_list(list);
|
||||
};
|
||||
Effect::new(move |_| {
|
||||
let child_map = map_signal.get();
|
||||
let mut list = data_list.get_untracked();
|
||||
update_entry_list(&mut list, &child_map);
|
||||
update_parent_list(list);
|
||||
});
|
||||
|
||||
view! {
|
||||
<Entry
|
||||
data_map=map_signal
|
||||
schema=schema
|
||||
id=unique_id
|
||||
on_delete=on_delete
|
||||
/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<ActionButton on:click = add_entry>"Add entry"</ActionButton>
|
||||
<ActionButton on:click=move |_| {
|
||||
console_log(format!("Entry list: {:?}", data_reactive.get()).as_str());
|
||||
}
|
||||
>"Print Info"</ActionButton>
|
||||
<ActionButton on:click=add_entry>"Add entry"</ActionButton>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -176,20 +207,35 @@ fn Entry(
|
||||
/// The data map that contains all information. Data is found within this map using the `member`
|
||||
/// props. This map should be unique to this entry; in other words, it should be contained and
|
||||
/// passed down from within the entry list in the parent `MultiEntry` component.
|
||||
#[prop()]
|
||||
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
/// The schema of this entry. Should contain all information about what inputs to use and what
|
||||
/// data members those inputs modify.
|
||||
#[prop()]
|
||||
schema: Vec<InputType>,
|
||||
#[prop(into)] schema: Signal<Vec<InputType>>,
|
||||
/// The unique ID for this entry.
|
||||
#[prop(into)] id: Signal<String>,
|
||||
/// The function to run when the delete button is clicked.
|
||||
#[prop(into)] on_delete: Callback<()>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<div class="flex flex-col flex-1 p-2 rounded-sm transition-shadow shadow-sm hover:shadow-lg/30">
|
||||
{schema.iter()
|
||||
.map(|input_type| {
|
||||
input_type.clone().into_view(data_map.clone())
|
||||
}).collect_view()
|
||||
}
|
||||
<div class="relative rounded-sm transition-shadow shadow-sm hover:shadow-lg/30">
|
||||
// Delete button
|
||||
<div
|
||||
class="absolute rounded-full w-8 h-8 -top-4 -right-4 flex items-center justify-center bg-gray-100 text-gray-700 hover:text-red-600 transition-all cursor-pointer"
|
||||
on:click=move |_| on_delete.run(())
|
||||
>
|
||||
<Icon icon=icondata::FaTrashCanRegular />
|
||||
</div>
|
||||
|
||||
// Input section
|
||||
<div class="flex flex-col flex-1 p-2">
|
||||
{move || {
|
||||
schema
|
||||
.get()
|
||||
.iter()
|
||||
.map(|input_type| input_type.clone().into_view(data_map.clone(), id.get()))
|
||||
.collect_view()
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod sidebar;
|
||||
pub use sidebar::*;
|
||||
@@ -0,0 +1,141 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::{Outlet, A};
|
||||
use leptos_router::hooks::use_location;
|
||||
use crate::common::{SubTabInfo, TabInfo};
|
||||
|
||||
/// # Tab Sidebar Component
|
||||
///
|
||||
/// This component creates a list of tabs that correspond to the given `Vec<TabInfo>`.
|
||||
/// There are a few things to note:
|
||||
///
|
||||
/// - This component defines an [`Outlet`], meaning that it is designed to display the route
|
||||
/// information on its own. If you intend to use this on a page, the page should not also contain
|
||||
/// an `Outlet` component unless it needs to render its own nested children.
|
||||
/// - The "selected" appearance for the individual tabs work best when their paths are at the same
|
||||
/// depth. For example:
|
||||
/// ```
|
||||
/// let good_list = vec! [
|
||||
/// TabInfo {
|
||||
/// ...,
|
||||
/// path: "/default/path"
|
||||
/// },
|
||||
/// TabInfo {
|
||||
/// ...,
|
||||
/// path: "/good/path"
|
||||
/// }
|
||||
/// ];
|
||||
///
|
||||
/// let bad_list = vec! [
|
||||
/// TabInfo {
|
||||
/// ...,
|
||||
/// path: "/default/path"
|
||||
/// },
|
||||
/// TabInfo {
|
||||
/// ...,
|
||||
/// path: "/bad/path/here"
|
||||
/// }
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// Example usage in a view (assuming the good list from above):
|
||||
/// ```
|
||||
/// view! {
|
||||
/// <TabSidebarList
|
||||
/// tabs=good_list
|
||||
/// />
|
||||
/// // Remember, no Outlets!
|
||||
/// }
|
||||
/// ```
|
||||
#[component]
|
||||
pub fn TabSidebarList(
|
||||
#[prop(into)] base_path: Signal<String>,
|
||||
#[prop(into)] tabs: Signal<Vec<TabInfo>>,
|
||||
) -> impl IntoView {
|
||||
// Create a series of tabs based on a given list of names and paths.
|
||||
|
||||
view! {
|
||||
<div class="flex flex-1 flex-row w-full">
|
||||
<nav class="flex flex-col bg-red-900 w-64 overflow-scroll">
|
||||
<For
|
||||
each=move || tabs.get()
|
||||
key=|info| info.text.clone()
|
||||
let(TabInfo { text, path, sub_paths })
|
||||
>
|
||||
<SidebarTab base_path=base_path text=text path=path sub_paths=sub_paths />
|
||||
</For>
|
||||
</nav>
|
||||
|
||||
<div class="flex flex-1 overflow-scroll">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SidebarTab(
|
||||
#[prop(into)] base_path: Signal<String>,
|
||||
#[prop(into)] text: Signal<String>,
|
||||
#[prop(into)] path: Signal<String>,
|
||||
#[prop(into)] sub_paths: Signal<Vec<SubTabInfo>>,
|
||||
) -> impl IntoView {
|
||||
let location = use_location();
|
||||
|
||||
let full_path = Memo::new(move |_| {
|
||||
format!("/{}/{}", base_path.get(), path.get())
|
||||
});
|
||||
|
||||
let selected = Memo::new(move |_| {
|
||||
let current_path = location.pathname.get();
|
||||
|
||||
// There's extra logic here as well, but for now this works.
|
||||
current_path.starts_with(&full_path.get())
|
||||
});
|
||||
|
||||
view! {
|
||||
<div
|
||||
class="transition-all duration-300"
|
||||
class=(["bg-transparent", "text-white"], move || !selected.get())
|
||||
class=(["bg-white", "text-black"], move || selected.get())
|
||||
>
|
||||
<A href=move || path.get()>
|
||||
<div class="p-3 transition-bg font-bold">{text}</div>
|
||||
</A>
|
||||
<div
|
||||
class="grid transition-all ease-in-out"
|
||||
style:grid-template-rows=move || if selected.get() { "1fr" } else { "0fr" }
|
||||
>
|
||||
<div class="overflow-hidden min-h-0">
|
||||
<For
|
||||
each=move || sub_paths.get()
|
||||
key=|info| info.text.clone()
|
||||
let(SubTabInfo { text, path })
|
||||
>
|
||||
<SidebarSubTab parent_path=full_path text=text path=path />
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SidebarSubTab(
|
||||
#[prop(into)] parent_path: Signal<String>,
|
||||
#[prop(into)] text: Signal<String>,
|
||||
#[prop(into)] path: Signal<String>,
|
||||
) -> impl IntoView {
|
||||
let full_path = Memo::new(move |_| {
|
||||
format!("{}/{}", parent_path.get(), path.get())
|
||||
});
|
||||
|
||||
view! {
|
||||
<A
|
||||
href=move || full_path.get()
|
||||
{..}
|
||||
class="block p-3 pl-10 transition-all aria-[current]:font-bold"
|
||||
>
|
||||
{text}
|
||||
</A>
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,15 @@ pub enum TextFieldType {
|
||||
Text,
|
||||
/// A text field that accepts only numbers.
|
||||
Number,
|
||||
/// A text field that accepts only Region 15 email addresses.
|
||||
/// TODO Accept more than just Region 15 addresses.
|
||||
Email
|
||||
/// A text field that accepts the specified email domains.
|
||||
/// Values should be given as the string following the '@' in an address, or '*' for any.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```
|
||||
/// TextFieldType::Email(vec!["gmail.com".to_string(), "customdomain.org".to_string()]);
|
||||
/// TextFieldType::Email(vec!["*".to_string()]);
|
||||
/// ```
|
||||
Email(Vec<String>)
|
||||
}
|
||||
|
||||
/// Entry point for validating a text field.
|
||||
@@ -24,7 +30,7 @@ fn validate(required: bool, value: &str, input_type: &TextFieldType) -> Validati
|
||||
ValidationState::Invalid("This field is required.".to_string())
|
||||
} else {
|
||||
match input_type {
|
||||
TextFieldType::Email => validate_email(value),
|
||||
TextFieldType::Email(domains) => validate_email(value, domains),
|
||||
TextFieldType::Number => validate_number(value),
|
||||
TextFieldType::Text => ValidationState::Valid
|
||||
}
|
||||
@@ -32,15 +38,18 @@ fn validate(required: bool, value: &str, input_type: &TextFieldType) -> Validati
|
||||
}
|
||||
|
||||
/// Validates an email address.
|
||||
fn validate_email(input: &str) -> ValidationState {
|
||||
// TODO Accept more than just Region 15 addresses.
|
||||
let pattern = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@region15\.org$").expect("Invalid regex pattern");
|
||||
fn validate_email(input: &str, valid_domains: &Vec<String>) -> ValidationState {
|
||||
// Check for wildcard domain. We won't check domains if it's present.
|
||||
let wild = valid_domains.contains(&"*".to_string());
|
||||
|
||||
if pattern.is_match(input) {
|
||||
ValidationState::Valid
|
||||
} else {
|
||||
ValidationState::Invalid("Please provide a valid Region 15 email address.".to_string())
|
||||
}
|
||||
input.split_once('@')
|
||||
.and_then(|(_, domain)| {
|
||||
(wild || valid_domains.contains(&domain.to_string()))
|
||||
.then(|| ValidationState::Valid)
|
||||
.or_else(|| Some(ValidationState::Invalid(
|
||||
format!("Email address must match one of the following: {}", valid_domains.join(", "))
|
||||
)))
|
||||
}).unwrap_or(ValidationState::Invalid("Invalid email address.".to_string()))
|
||||
}
|
||||
|
||||
/// Validates a number.
|
||||
@@ -135,12 +144,12 @@ pub fn OutlinedTextField(
|
||||
<input
|
||||
node_ref=input_ref
|
||||
class="border-2 m-1.5 p-1.5 mt-0 rounded-md bg-transparent relative flex-1
|
||||
transition-all duration-150
|
||||
border-red-700 bg-transparent
|
||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
r#type={str_type}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
transition-all duration-150
|
||||
border-red-700 bg-transparent
|
||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
r#type=str_type
|
||||
disabled=disabled
|
||||
placeholder=placeholder
|
||||
prop:name=name
|
||||
prop:value=raw_value
|
||||
on:input=on_input
|
||||
@@ -151,7 +160,7 @@ pub fn OutlinedTextField(
|
||||
{move || {
|
||||
match error.get() {
|
||||
ValidationState::Invalid(msg) => msg,
|
||||
_ => "There is no error - should not see this message.".to_string()
|
||||
_ => "There is no error - should not see this message.".to_string(),
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::components::{ActionButton, Header};
|
||||
|
||||
/// Indicates the current state of an input.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum ValidationState {
|
||||
/// A valid input.
|
||||
#[default]
|
||||
@@ -149,11 +150,8 @@ pub fn ValidatedForm(
|
||||
};
|
||||
|
||||
view! {
|
||||
<form novalidate on:submit=submit_success_event>
|
||||
<Header
|
||||
title=title
|
||||
description=description
|
||||
/>
|
||||
<form class="flex flex-col gap-2 py-7" novalidate on:submit=submit_success_event>
|
||||
<Header title=title description=description />
|
||||
{children()}
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod app;
|
||||
mod common;
|
||||
mod components;
|
||||
mod pages;
|
||||
pub mod utils;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
|
||||
+19
-4
@@ -6,7 +6,12 @@ async fn main() {
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::{LeptosRoutes, generate_route_list};
|
||||
use scholarships_rs::app::*;
|
||||
use scholarships_rs::utils::server::create_dynamo_client;
|
||||
use std::sync::Arc;
|
||||
|
||||
let dynamo_client = create_dynamo_client().await;
|
||||
let shared_client = Arc::new(dynamo_client);
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
let leptos_options = conf.leptos_options;
|
||||
@@ -14,10 +19,20 @@ async fn main() {
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
let app = Router::new()
|
||||
.leptos_routes(&leptos_options, routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.leptos_routes_with_context(
|
||||
&leptos_options,
|
||||
routes,
|
||||
{
|
||||
let shared_client = Arc::clone(&shared_client);
|
||||
move || {
|
||||
provide_context(Arc::clone(&shared_client));
|
||||
}
|
||||
},
|
||||
{
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
}
|
||||
)
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn AdminHomePage() -> impl IntoView {
|
||||
view! {
|
||||
<div class="flex flex-1 flex-col gap-4 items-center m-4">
|
||||
<div class="font-bold text-3xl">"Admin Home Page"</div>
|
||||
<div class="text-lg">"This is just a placeholder for now."</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod shell;
|
||||
mod home_page;
|
||||
mod provider_page;
|
||||
mod scholarship_page;
|
||||
mod utils_page;
|
||||
|
||||
pub use shell::*;
|
||||
pub use home_page::*;
|
||||
pub use provider_page::*;
|
||||
pub use scholarship_page::*;
|
||||
pub use utils_page::*;
|
||||
@@ -0,0 +1,83 @@
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use crate::common::ValueType;
|
||||
use crate::components::{NavListInfo, SelectableNavList};
|
||||
use crate::pages::api::get_all_providers;
|
||||
|
||||
#[component]
|
||||
pub fn AdminProviderPage() -> impl IntoView {
|
||||
let selected_items = RwSignal::new(Vec::new());
|
||||
let provider_list = RwSignal::new(Vec::new());
|
||||
|
||||
let provider_trigger = Trigger::new();
|
||||
let provider_resource = Resource::new(
|
||||
move || provider_trigger.track(),
|
||||
async move |_| {
|
||||
get_all_providers().await
|
||||
}
|
||||
);
|
||||
|
||||
Effect::new(move || {
|
||||
if let Some(Err(err)) = provider_resource.get() {
|
||||
log!("Found error: {}", err.to_string());
|
||||
}
|
||||
let Some(Ok(list)) = provider_resource.get() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let members_list = vec![
|
||||
"contact_email".to_string(),
|
||||
"first_name".to_string(),
|
||||
"last_name".to_string(),
|
||||
];
|
||||
|
||||
// Convert the list into a series of NavListInfo
|
||||
let converted = list
|
||||
.into_iter()
|
||||
.map(|info| {
|
||||
let mut text_list = vec![];
|
||||
members_list.iter().for_each(|member| {
|
||||
let val = info.get(member)
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string()
|
||||
.ok().flatten()
|
||||
.unwrap_or_default();
|
||||
text_list.push(val);
|
||||
});
|
||||
|
||||
let subject = info.get("subject")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.to_string();
|
||||
|
||||
NavListInfo {
|
||||
text: text_list,
|
||||
id: subject.clone(),
|
||||
slug: subject,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
provider_list.set(converted);
|
||||
});
|
||||
|
||||
let headers = vec![
|
||||
"Email",
|
||||
"First Name",
|
||||
"Last Name"
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
view! {
|
||||
<div class="flex flex-1 flex-col gap-4 items-stretch mt-4">
|
||||
<div class="font-bold text-3xl self-center">"Provider View"</div>
|
||||
<SelectableNavList
|
||||
selected=selected_items
|
||||
name="providers-nav-list"
|
||||
items=provider_list
|
||||
headers=headers
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn AdminScholarshipPage() -> impl IntoView {
|
||||
view! {
|
||||
<div class="flex flex-1 flex-col gap-4">
|
||||
<div class="font-bold text-3xl">
|
||||
"Scholarships View"
|
||||
</div>
|
||||
<div>
|
||||
"This is a placeholder for now. It will show a list of all scholarships."
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::common::TabInfo;
|
||||
use crate::components::{Banner, TabSidebarList};
|
||||
use crate::components::login::AdminLoginContext;
|
||||
|
||||
/// # Admin Shell Component
|
||||
///
|
||||
/// This shell should keep track of pretty much everything that changes across the admin page. It
|
||||
/// will provide the login context, the banner, and the tabs.
|
||||
#[component]
|
||||
pub fn AdminShell() -> impl IntoView {
|
||||
// Create login context
|
||||
view! {
|
||||
<AdminLoginContext>
|
||||
<Banner title="R15 Scholarships Admin" logo="/PHS_Stacked_Acronym.png" />
|
||||
// We'll put the tabs here.
|
||||
<TabSidebarList
|
||||
base_path="admin"
|
||||
tabs=vec![
|
||||
TabInfo::new("Home", "home", None),
|
||||
TabInfo::new("Providers", "providers", None),
|
||||
TabInfo::new("Scholarships", "scholarships", None),
|
||||
TabInfo::new("Utilities", "utilities", None),
|
||||
]
|
||||
/>
|
||||
</AdminLoginContext>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
use chrono::DateTime;
|
||||
use crate::components::ActionButton;
|
||||
use leptos::prelude::*;
|
||||
use crate::common::{DateInfo, DateRange};
|
||||
use crate::pages::api::{CreateDates, CreateTestComparisons};
|
||||
|
||||
fn get_important_dates() -> Vec<DateInfo> {
|
||||
vec! [
|
||||
DateInfo {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
title: "Provider Forms Open".to_string(),
|
||||
date: DateRange::Range(
|
||||
DateTime::parse_from_rfc3339("2026-01-20T07:15:00-05:00").unwrap(),
|
||||
DateTime::parse_from_rfc3339("2026-03-03T23:59:00-05:00").unwrap()
|
||||
),
|
||||
description: "".to_string(),
|
||||
},
|
||||
DateInfo {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
title: "Student Forms Open".to_string(),
|
||||
date: DateRange::Range(
|
||||
DateTime::parse_from_rfc3339("2026-03-03T07:15:00-05:00").unwrap(),
|
||||
DateTime::parse_from_rfc3339("2026-03-27T14:05:00-05:00").unwrap()
|
||||
),
|
||||
description: "".to_string(),
|
||||
},
|
||||
DateInfo {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
title: "Scholarship Decision Window".to_string(),
|
||||
date: DateRange::Range(
|
||||
DateTime::parse_from_rfc3339("2026-04-10T07:15:00-05:00").unwrap(),
|
||||
DateTime::parse_from_rfc3339("2026-05-01T23:59:00-05:00").unwrap()
|
||||
),
|
||||
description: "Start date may change if available sooner.".to_string(),
|
||||
},
|
||||
DateInfo {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
title: "PHS Scholarship Committee".to_string(),
|
||||
date: DateRange::Single(
|
||||
DateTime::parse_from_rfc3339("2026-05-18T00:00:00-05:00").unwrap(),
|
||||
),
|
||||
description: "Time TBD".to_string(),
|
||||
},
|
||||
DateInfo {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
title: "Scholarship and Awards Night".to_string(),
|
||||
date: DateRange::Single(
|
||||
DateTime::parse_from_rfc3339("2026-06-09T00:00:00-05:00").unwrap(),
|
||||
),
|
||||
description: "Time TBD".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn create_scholarship_export() -> Result<Vec<u8>, ServerFnError> {
|
||||
// Exports olarship information - specifically the name of the scholarship, the name of the
|
||||
// provider, and the scholarship's requirements.
|
||||
// This means we'll need to get all the relations, all the scholarships, and all the contact info.
|
||||
use crate::utils::server::create_dynamo_client;
|
||||
use aws_sdk_dynamodb::client::Client;
|
||||
use crate::pages::api::{get_comparison_info, SCHOLARSHIPS_TABLE, PROVIDER_CONTACT_TABLE};
|
||||
use std::collections::HashMap;
|
||||
use crate::common::ValueType;
|
||||
|
||||
let scan_table_items = async |client: &Client, table_name: &str| {
|
||||
client.scan()
|
||||
.table_name(table_name)
|
||||
.send()
|
||||
.await
|
||||
.ok().and_then(|res| res.items)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
struct ScholarshipInfo {
|
||||
name: String,
|
||||
provider_name: String,
|
||||
relations: Vec<String>,
|
||||
}
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
let scholarships = dbg!(scan_table_items(&client, SCHOLARSHIPS_TABLE)
|
||||
.await
|
||||
.iter().map(|item| {
|
||||
item.into_iter()
|
||||
.map(|(k, v)| (k.clone(), ValueType::from(v)))
|
||||
.collect()
|
||||
}).collect::<Vec<HashMap<String, ValueType>>>()
|
||||
);
|
||||
let contacts = dbg!(
|
||||
scan_table_items(&client, PROVIDER_CONTACT_TABLE).await
|
||||
.iter().map(|item| {
|
||||
item.into_iter()
|
||||
.map(|(k, v)| (k.clone(), ValueType::from(v)))
|
||||
.collect()
|
||||
}).collect::<Vec<HashMap<String, ValueType>>>()
|
||||
);
|
||||
let db_relations = dbg!(
|
||||
get_comparison_info().await.unwrap_or_default()
|
||||
);
|
||||
|
||||
// Build a new structure containing some of the scholarship's information.
|
||||
let resolved = scholarships.into_iter()
|
||||
.map(|scholarship| {
|
||||
// We just want the scholarship's name, the relations, and the provider's name.
|
||||
// In here, we're going to resolve all the scholarship's information to provide this in
|
||||
// a readable format.
|
||||
let name = scholarship.get("name")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.to_string();
|
||||
|
||||
// The list of relations - we're going to search the master list for each comparison's
|
||||
// name. It's structured as a map of lists, or a ValueType::Map(Option<HashMap<String, ValueType::List(Option<Vec<ValueType::String()>>)>>)
|
||||
let relations = scholarship.get("requirements")
|
||||
.unwrap_or(&ValueType::Map(None))
|
||||
.as_map().ok().flatten()
|
||||
.unwrap_or_default() // We have our map of categories, now we need all the values inside each category.
|
||||
.iter()
|
||||
.map(|(_, list)| {
|
||||
list.as_list().ok().flatten()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<ValueType>>() // This is a Vec<ValueType::String>
|
||||
.iter().map(|val| val.clone().as_string().ok().flatten().unwrap_or_default())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// The provider ID, which we'll use to search for the provider's name.
|
||||
let provider_id = scholarship.get("provider_id")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.to_string();
|
||||
|
||||
// This is a mess, we'll clean it up later.
|
||||
let parsed_relations = relations
|
||||
.iter()
|
||||
.filter_map(|relation| {
|
||||
db_relations.iter().find(|search| {
|
||||
search.id == *relation
|
||||
})
|
||||
.and_then(|search| Some(search.display_text.clone()))
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
ScholarshipInfo {
|
||||
name,
|
||||
provider_name: provider_id,
|
||||
relations: parsed_relations,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<ScholarshipInfo>>();
|
||||
|
||||
// TODO Convert into CSV and send to client. For now I think I'm just going to serialize to json and print it.
|
||||
leptos::logging::log!("Scholarships list: {:?}", resolved);
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AdminUtilsPage() -> impl IntoView {
|
||||
let create_comparisons = ServerAction::<CreateTestComparisons>::new();
|
||||
let create_dates = ServerAction::<CreateDates>::new();
|
||||
let create_export = ServerAction::<CreateScholarshipExport>::new();
|
||||
|
||||
let on_click_comparisons = move |_| {
|
||||
create_comparisons.dispatch(CreateTestComparisons{});
|
||||
};
|
||||
|
||||
let on_click_dates = move |_| {
|
||||
create_dates.dispatch(CreateDates{dates: get_important_dates()});
|
||||
};
|
||||
|
||||
let on_click_export = move |_| {
|
||||
create_export.dispatch(CreateScholarshipExport{});
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="mx-auto px-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="self-centered text-lg mt-6">
|
||||
"This utility page provides buttons to initialize the corresponding lists of information."
|
||||
</div>
|
||||
<ActionButton on:click=on_click_comparisons>"Create Comparisons"</ActionButton>
|
||||
<ActionButton on:click=on_click_dates>"Create Dates"</ActionButton>
|
||||
<ActionButton on:click=on_click_export>"Scholarship Export"</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
}.into_any()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use leptos::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use crate::common::ValueType;
|
||||
use leptos::logging::log;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod server_logic {
|
||||
pub use crate::utils::server::create_dynamo_client;
|
||||
pub use super::super::PROVIDER_CONTACT_TABLE;
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_all_providers() -> Result<Vec<HashMap<String, ValueType>>, ServerFnError> {
|
||||
use server_logic::*;
|
||||
|
||||
// We want to get all provider information from the contacts table.
|
||||
// This will be migrated to the new table structure when it happens.
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Scanning all provider information from {}", PROVIDER_CONTACT_TABLE);
|
||||
|
||||
client
|
||||
.scan()
|
||||
.table_name(PROVIDER_CONTACT_TABLE)
|
||||
.send()
|
||||
.await
|
||||
.map(|output| {
|
||||
if let Some(items) = output.items {
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
item.into_iter().map(|(key, attr)| {
|
||||
let val = ValueType::from(&attr);
|
||||
(key, val)
|
||||
}).collect()
|
||||
})
|
||||
.collect::<Vec<HashMap<String, ValueType>>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
})
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
+134
-30
@@ -1,6 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::pages::utils::server_utils::create_dynamo_client;
|
||||
use crate::utils::server::create_dynamo_client;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
@@ -8,13 +7,18 @@ use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::logging::log;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use super::COMPARISONS_TABLE;
|
||||
|
||||
use crate::common::{
|
||||
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
|
||||
TextComparison, TextListComparison, ValueType,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use leptos::server;
|
||||
|
||||
|
||||
/// Helper function to create all sports comparisons. Only used to create initial
|
||||
/// comparison data, and should be removed upon completion of a comparison editor page.
|
||||
#[allow(unused)]
|
||||
@@ -185,18 +189,87 @@ fn create_sports_comparisons() -> Vec<ComparisonData> {
|
||||
"Sports Participation",
|
||||
"Tennis",
|
||||
),
|
||||
ComparisonData::new(
|
||||
"sports_baseball",
|
||||
"sports_participation",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToTextList(
|
||||
"sport_name".to_string(),
|
||||
Box::new(TextListComparison::Contains),
|
||||
)),
|
||||
ValueType::String(Some("Baseball".to_string())),
|
||||
"Sports Participation",
|
||||
"Baseball",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn create_major_comparison(
|
||||
id: impl Into<String>,
|
||||
display_text: impl Into<String>,
|
||||
) -> ComparisonData {
|
||||
let display = display_text.into();
|
||||
ComparisonData::new(
|
||||
id,
|
||||
"major",
|
||||
ComparisonType::Text(TextComparison::Contains),
|
||||
ValueType::String(Some(display.clone())),
|
||||
"Majors",
|
||||
display,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_major_comparisons() -> Vec<ComparisonData> {
|
||||
let display_texts = vec![
|
||||
"Music",
|
||||
"Education",
|
||||
"Special Education",
|
||||
"Speech Pathology",
|
||||
"School Psychology",
|
||||
"School Counseling",
|
||||
"Occupational Therapy",
|
||||
"Physical Therapy",
|
||||
"Nursing",
|
||||
"Allied Health",
|
||||
"Fine/Performing Arts",
|
||||
"Writing/Communication",
|
||||
"History",
|
||||
"Government",
|
||||
"Political Science",
|
||||
"Social Work",
|
||||
"Sports Medicine",
|
||||
"Athletic Training",
|
||||
"Horticulture",
|
||||
"Conservation Studies",
|
||||
"Ecology",
|
||||
"Environmental Studies",
|
||||
"Urban Planning",
|
||||
"Landscaping",
|
||||
"Legal Studies",
|
||||
"Criminal Justice",
|
||||
];
|
||||
|
||||
let ids = display_texts
|
||||
.iter()
|
||||
.map(|text|
|
||||
format!("{}_{}", "major", text.to_lowercase().replace(" ", "_").replace("/", "_"))
|
||||
)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
ids.iter().zip(display_texts).map(|(id, display_text)| {
|
||||
create_major_comparison(id, display_text)
|
||||
})
|
||||
.collect::<Vec<ComparisonData>>()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn make_comp_list() -> Vec<ComparisonData> {
|
||||
let gpa_3 = ComparisonData::new(
|
||||
"gpa_3",
|
||||
"gpa_top_third",
|
||||
"weighted_gpa",
|
||||
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
|
||||
ValueType::Number(Some(3.0.to_string())),
|
||||
ValueType::Number(Some(6.264.to_string())),
|
||||
"GPA Limits",
|
||||
"GPA >= 3.0",
|
||||
"Top Third GPA",
|
||||
);
|
||||
|
||||
let test_comp = ComparisonData::new(
|
||||
@@ -210,13 +283,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
|
||||
let service_hours_20 = ComparisonData::new(
|
||||
"service_hours_20",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
"service_hours",
|
||||
ComparisonType::Number(
|
||||
NumberComparison::GreaterThanOrEqual
|
||||
),
|
||||
ValueType::Number(Some(20.to_string())),
|
||||
"Community Service",
|
||||
"20+ service hours",
|
||||
@@ -224,13 +294,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
|
||||
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,
|
||||
))),
|
||||
)),
|
||||
"service_hours",
|
||||
ComparisonType::Number(
|
||||
NumberComparison::GreaterThanOrEqual
|
||||
),
|
||||
ValueType::Number(Some(25.to_string())),
|
||||
"Community Service",
|
||||
"25+ service hours",
|
||||
@@ -238,13 +305,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
|
||||
let service_hours_30 = ComparisonData::new(
|
||||
"service_hours_30",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
"service_hours",
|
||||
ComparisonType::Number(
|
||||
NumberComparison::GreaterThanOrEqual
|
||||
),
|
||||
ValueType::Number(Some(30.to_string())),
|
||||
"Community Service",
|
||||
"30+ service hours",
|
||||
@@ -295,8 +359,44 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
"Family with Military Service",
|
||||
);
|
||||
|
||||
let youth_baseball = ComparisonData::new(
|
||||
"youth_baseball",
|
||||
"youth_baseball",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Additional Eligibility Factors",
|
||||
"Participated in Pomperaug Youth Baseball"
|
||||
);
|
||||
|
||||
let aquatic_club = ComparisonData::new(
|
||||
"aquatic_club",
|
||||
"aquatic_club",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Additional Eligibility Factors",
|
||||
"Participated in Panthers Aquatic Club"
|
||||
);
|
||||
|
||||
let pea_member = ComparisonData::new(
|
||||
"pea_member",
|
||||
"pea_member",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Additional Eligibility Factors",
|
||||
"Has a Parent in the PEA"
|
||||
);
|
||||
|
||||
let music_program = ComparisonData::new(
|
||||
"music_program",
|
||||
"music_program",
|
||||
ComparisonType::Text(TextComparison::Matches),
|
||||
ValueType::String(Some("Yes".to_string())),
|
||||
"Sports Participation",
|
||||
"PHS Music Program"
|
||||
);
|
||||
|
||||
let mut sports_comps = create_sports_comparisons();
|
||||
let mut major_comps = Vec::<ComparisonData>::new();
|
||||
let mut major_comps = create_major_comparisons();
|
||||
|
||||
let mut comp_list = vec![
|
||||
gpa_3,
|
||||
@@ -309,6 +409,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
attended_bas,
|
||||
midd_south,
|
||||
family_military,
|
||||
youth_baseball,
|
||||
aquatic_club,
|
||||
pea_member,
|
||||
music_program
|
||||
];
|
||||
|
||||
comp_list.append(&mut sports_comps);
|
||||
@@ -328,7 +432,7 @@ pub async fn create_test_comparisons() -> Result<(), ServerFnError> {
|
||||
for comparison in comp_list {
|
||||
if let Err(err) = client
|
||||
.put_item()
|
||||
.table_name("leptos-comparison-test")
|
||||
.table_name(COMPARISONS_TABLE)
|
||||
.set_item(Some(serde_dynamo::to_item(&comparison)?))
|
||||
.send()
|
||||
.await
|
||||
@@ -351,7 +455,7 @@ pub async fn get_comparison_info() -> Result<Vec<ComparisonData>, ServerFnError>
|
||||
// id and display text, but we'll return the whole thing.
|
||||
match client
|
||||
.scan()
|
||||
.table_name("leptos-comparison-test")
|
||||
.table_name(COMPARISONS_TABLE)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
@@ -377,7 +481,7 @@ pub async fn get_comparisons_categorized() -> Result<HashMap<String, Vec<Compari
|
||||
|
||||
match client
|
||||
.scan()
|
||||
.table_name("leptos-comparison-test")
|
||||
.table_name(COMPARISONS_TABLE)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use crate::common::DateInfo;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use super::DATES_TABLE;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::{
|
||||
types::{WriteRequest, PutRequest},
|
||||
Client as DynamoClient
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::utils::server::create_dynamo_client;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::Arc;
|
||||
|
||||
#[server]
|
||||
pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> {
|
||||
// Serialize and put dates into the database.
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Creating dates: {:?}", dates);
|
||||
|
||||
let requests = dates
|
||||
.into_iter()
|
||||
.map(|date| {
|
||||
let item = serde_dynamo::to_item(&date)
|
||||
.unwrap_or_default();
|
||||
|
||||
let put_request = PutRequest::builder()
|
||||
.set_item(Some(item))
|
||||
.build()
|
||||
.expect("Couldn't build request, item is not present");
|
||||
|
||||
WriteRequest::builder().put_request(put_request).build()
|
||||
})
|
||||
.collect::<Vec<WriteRequest>>();
|
||||
|
||||
let mut request_map = HashMap::new();
|
||||
request_map.insert(DATES_TABLE.to_string(), requests);
|
||||
|
||||
client
|
||||
.batch_write_item()
|
||||
.set_request_items(Some(request_map))
|
||||
.send()
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> {
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
client
|
||||
.scan()
|
||||
.table_name(DATES_TABLE)
|
||||
.send()
|
||||
.await
|
||||
.map(|output| {
|
||||
if let Some(items) = output.items {
|
||||
items
|
||||
.into_iter()
|
||||
.filter_map(|item| {
|
||||
serde_dynamo::from_item::<_, DateInfo>(item)
|
||||
.ok()
|
||||
})
|
||||
.collect::<Vec<DateInfo>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
})
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
+24
-1
@@ -1,4 +1,27 @@
|
||||
mod comparisons;
|
||||
mod scholarships;
|
||||
mod dates;
|
||||
mod admin;
|
||||
mod providers;
|
||||
pub mod students;
|
||||
|
||||
pub use self::{comparisons::*, scholarships::*};
|
||||
pub use comparisons::*;
|
||||
pub use scholarships::*;
|
||||
pub use dates::*;
|
||||
pub use admin::*;
|
||||
pub use providers::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub static COMPARISONS_TABLE: &str = "leptos-comparisons";
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub static DATES_TABLE: &str = "leptos-dates";
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub static SCHOLARSHIPS_TABLE: &str = "leptos-scholarships";
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub static MAIN_TABLE_NAME: &str = "scholarships-main";
|
||||
@@ -0,0 +1,71 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::logging::debug_log;
|
||||
use std::collections::HashMap;
|
||||
use leptos::server_fn::codec::Json;
|
||||
use crate::common::ValueType;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::utils::server::{create_dynamo_client, into_attr_map};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
|
||||
|
||||
#[server]
|
||||
pub 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;
|
||||
|
||||
debug_log!("Getting contact info for provider {}", id);
|
||||
|
||||
match client
|
||||
.get_item()
|
||||
.table_name(PROVIDER_CONTACT_TABLE)
|
||||
.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(input = Json)]
|
||||
pub async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> {
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
|
||||
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(PROVIDER_CONTACT_TABLE)
|
||||
.set_item(Some(item))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||
Err(ServerFnError::new(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,26 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::{
|
||||
types::AttributeValue,
|
||||
error::ProvideErrorMetadata,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::leptos_dom::log;
|
||||
#[cfg(feature = "ssr")]
|
||||
use uuid::Uuid;
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::pages::utils::server_utils::create_dynamo_client;
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::common::ValueType;
|
||||
mod imports {
|
||||
pub use leptos::logging::debug_log;
|
||||
pub use aws_sdk_dynamodb::{
|
||||
types::AttributeValue,
|
||||
error::ProvideErrorMetadata,
|
||||
};
|
||||
pub use leptos::leptos_dom::log;
|
||||
pub use uuid::Uuid;
|
||||
pub use crate::utils::server::{create_dynamo_client};
|
||||
pub use crate::common::ValueType;
|
||||
pub use super::super::SCHOLARSHIPS_TABLE;
|
||||
}
|
||||
|
||||
use crate::common::ExpandableInfo;
|
||||
|
||||
use leptos::prelude::ServerFnError;
|
||||
use leptos::server;
|
||||
use leptos::server_fn::codec::Json;
|
||||
|
||||
#[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")]
|
||||
#[server(GetScholarshipInfo)]
|
||||
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
// Perform the operation - we just want to return all data that's contained in this entry,
|
||||
@@ -26,7 +28,7 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
|
||||
log!("Getting scholarship info using id {:?}", id);
|
||||
match client
|
||||
.get_item()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.key("subject", AttributeValue::S(id.clone()))
|
||||
.send()
|
||||
.await
|
||||
@@ -47,9 +49,9 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
|
||||
}
|
||||
}
|
||||
|
||||
#[server(CreateScholarshipInfo, endpoint = "/scholarship/info/create")]
|
||||
#[server(CreateScholarshipInfo, input = Json)]
|
||||
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> {
|
||||
use crate::pages::utils::server_utils::create_dynamo_client;
|
||||
use imports::*;
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
@@ -58,7 +60,7 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
|
||||
|
||||
match client
|
||||
.put_item()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.set_item(Some(serde_dynamo::to_item(&info)?))
|
||||
.send()
|
||||
.await
|
||||
@@ -71,13 +73,15 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
|
||||
}
|
||||
}
|
||||
|
||||
#[server(GetAllScholarshipInfo, endpoint = "/scholarship/info/get-all")]
|
||||
#[server(GetAllScholarshipInfo)]
|
||||
pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
log!("Getting all scholarship info");
|
||||
debug_log!("Getting all scholarship info");
|
||||
match client
|
||||
.scan()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
@@ -96,15 +100,17 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
|
||||
}
|
||||
|
||||
/// Gets all scholarships that are associated with the given scholarship provider's ID.
|
||||
#[server(GetProviderScholarships, "/providers/scholarships/get")]
|
||||
#[server(GetProviderScholarships)]
|
||||
pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Getting provider scholarships for provider with ID {:?}", provider_id);
|
||||
debug_log!("Getting provider scholarships for provider with ID {:?}", provider_id);
|
||||
|
||||
match client
|
||||
.scan()
|
||||
.table_name("leptos-scholarship-test")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.expression_attribute_values(":id", serde_dynamo::to_attribute_value(ValueType::String(Some(provider_id)))?)
|
||||
.filter_expression("provider_id = :id")
|
||||
.send()
|
||||
@@ -125,21 +131,22 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
|
||||
}
|
||||
|
||||
/// 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<String, ServerFnError> {
|
||||
#[server(RegisterScholarship)]
|
||||
pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Creating scholarship for provider with ID {:?}", provider_id);
|
||||
debug_log!("Creating scholarship for provider with ID {:?}", provider_id);
|
||||
|
||||
let mut current_uuid = "testing".to_string();
|
||||
let mut current_uuid = Uuid::new_v4().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")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.set_item(Some(ser_item))
|
||||
.condition_expression("attribute_not_exists(subject)")
|
||||
.send()
|
||||
@@ -170,17 +177,19 @@ pub async fn register_scholarship(provider_id: String, scholarship_name: String)
|
||||
}
|
||||
|
||||
/// Deletes a provider's scholarship given their provider ID and scholarship ID.
|
||||
#[server(DeleteProviderScholarship, "/providers/scholarships/delete")]
|
||||
#[server(DeleteProviderScholarship)]
|
||||
pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: String) -> Result<(), ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
log!("Deleting scholarship with ID {:?} for provider with ID {:?}", scholarship_id, provider_id);
|
||||
debug_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")
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.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")
|
||||
@@ -196,3 +205,29 @@ pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: St
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||
use imports::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
client
|
||||
.scan()
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
.send()
|
||||
.await
|
||||
.map(|output| {
|
||||
let Some(items) = output.items else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
items.iter().map(|item| {
|
||||
// Each item should be simply converted from an AttributeValue to a ValueType.
|
||||
item.iter().map(|(k, v)| (k.clone(), ValueType::from(v)))
|
||||
.collect::<HashMap<String, ValueType>>()
|
||||
}).collect::<Vec<_>>()
|
||||
})
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::server_fn::codec::Json;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod imports {
|
||||
pub use crate::utils::server::*;
|
||||
pub use aws_sdk_dynamodb::types::AttributeValue;
|
||||
pub use super::super::MAIN_TABLE_NAME;
|
||||
pub use crate::common::ValueType;
|
||||
pub use std::collections::HashMap;
|
||||
pub use leptos::logging::debug_log;
|
||||
}
|
||||
|
||||
#[server(input = Json)]
|
||||
pub async fn put_student_data(
|
||||
subject: String,
|
||||
data_type: String,
|
||||
data_map: std::collections::HashMap<String, crate::common::ValueType>
|
||||
) -> Result<(), ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
let mut data_map_attr = into_attr_map(data_map);
|
||||
data_map_attr.insert(
|
||||
"HK".into(),
|
||||
AttributeValue::S(format!("STUDENT#{}", subject))
|
||||
);
|
||||
data_map_attr.insert(
|
||||
"SK".into(),
|
||||
AttributeValue::S(data_type.to_uppercase())
|
||||
);
|
||||
|
||||
debug_log!("Inserting this item: {:?}", data_map_attr);
|
||||
|
||||
client
|
||||
.put_item()
|
||||
.table_name(MAIN_TABLE_NAME)
|
||||
.set_item(Some(data_map_attr))
|
||||
.send()
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn get_student_data(
|
||||
subject: String,
|
||||
data_type: String,
|
||||
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
client
|
||||
.query()
|
||||
.table_name(MAIN_TABLE_NAME)
|
||||
.key_condition_expression("HK = :hk AND SK = :sk")
|
||||
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject)))
|
||||
.expression_attribute_values(":sk", AttributeValue::S(data_type.to_uppercase()))
|
||||
.send()
|
||||
.await
|
||||
.map(|output| {
|
||||
// Map the output to a HashMap<String, ValueType>
|
||||
let Some(list) = output.items else {
|
||||
return HashMap::new();
|
||||
};
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
let _ = list
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|(k, v)| {
|
||||
map.insert(k, ValueType::from(&v));
|
||||
});
|
||||
|
||||
map
|
||||
})
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
|
||||
/// Gets and flattens all of a student's information from the database, regardless of their defined
|
||||
/// sort key.
|
||||
///
|
||||
/// Note: if sorted data from the `put_student_data` function contains the same fields, this
|
||||
/// function will have unexpected behavior. For example, if a `first_name` field appears in multiple
|
||||
/// locations in the database, all for this single student, they will overwrite each other in no
|
||||
/// specific order.
|
||||
#[server]
|
||||
pub async fn get_all_student_data(
|
||||
subject: String
|
||||
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
client.query()
|
||||
.table_name(MAIN_TABLE_NAME)
|
||||
.key_condition_expression("HK = :hk")
|
||||
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject)))
|
||||
.send()
|
||||
.await
|
||||
.map(|output| {
|
||||
let Some(items) = output.items else {
|
||||
return HashMap::new();
|
||||
};
|
||||
|
||||
items.iter().flatten()
|
||||
.map(|(|k, v)| (k.clone(), ValueType::from(v)))
|
||||
.collect::<HashMap<String, ValueType>>()
|
||||
})
|
||||
.map_err(ServerFnError::from)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::AuthSignal;
|
||||
use leptos_router::hooks::use_navigate;
|
||||
|
||||
#[component]
|
||||
pub fn AuthCallbackPage() -> impl IntoView {
|
||||
// This should wait for authentication, read the values in session storage, and then redirect
|
||||
// to the correct page. The session storage should contain a path, which we'll need to check.
|
||||
|
||||
let auth = expect_context::<AuthSignal>();
|
||||
let navigate = use_navigate();
|
||||
|
||||
Effect::new(move || {
|
||||
// Wait for auth to load, then read information.
|
||||
if let Some(_) = auth.get().authenticated() {
|
||||
// Read the sessionStorage to get the last navigated path.
|
||||
let storage_opt = window().session_storage().ok().flatten();
|
||||
let path = storage_opt
|
||||
.and_then(|storage| storage.get_item("post_login_path").ok().flatten())
|
||||
.unwrap_or_else(|| "/".to_string());
|
||||
|
||||
// Clear storage in case we need to reauth again
|
||||
if let Some(storage) = window().session_storage().ok().flatten() {
|
||||
let _ = storage.remove_item("post_login_path");
|
||||
}
|
||||
|
||||
// Redirect to the stored path
|
||||
navigate(&path, Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="flex flex-col w-full mx-auto items-center justify-center">
|
||||
<p class="text-xl font-bold mt-10 mx-auto">"Logging you in..."</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
use super::UnauthenticatedPage;
|
||||
use super::api::{get_all_scholarship_info, get_comparison_info};
|
||||
use super::home_page::get_submission;
|
||||
use crate::common::{
|
||||
ComparisonData, ExpandableInfo, 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::{AuthLoaded, Authenticated};
|
||||
use crate::pages::utils::get_user_claims;
|
||||
use super::api::{get_comparison_info, get_all_scholarship_info};
|
||||
use crate::utils::get_user_claims;
|
||||
|
||||
#[component]
|
||||
pub fn ComparisonTestPage() -> impl IntoView {
|
||||
@@ -59,93 +57,84 @@ pub fn ComparisonTestPage() -> impl IntoView {
|
||||
let student_info = student_resource.get();
|
||||
let comparison_info = comparison_resource.get();
|
||||
let scholarship_info = scholarship_resource.get();
|
||||
|
||||
match (student_info, comparison_info, scholarship_info) {
|
||||
(Some(student), Some(comparisons), Some(scholarships)) => Either::Right(view! {
|
||||
<Panel>
|
||||
<Row>
|
||||
<p>{result_msg}</p>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton on:click=move |_| {
|
||||
// Iterate over all scholarships, get the comparison IDs,
|
||||
// get ComparisonData from the IDs, then perform the
|
||||
// comparisons and return a boolean. Alternatively, return
|
||||
// a list of comparisons that complete and a list that don't.
|
||||
let comp_member = "requirements".to_string();
|
||||
let comparison_results = scholarships.iter().map(|scholarship| {
|
||||
let list = match scholarship.data.get(&comp_member) {
|
||||
Some(ValueType::List(Some(list))) => list,
|
||||
_ => {
|
||||
log!("Cannot find requirements, assuming none.");
|
||||
return Ok(Some(scholarship)) // students will always qualify when there are no requirements
|
||||
}
|
||||
};
|
||||
(Some(student), Some(comparisons), Some(scholarships)) => {
|
||||
Either::Right(
|
||||
|
||||
let mut final_result = Ok(true);
|
||||
|
||||
// The list should exist, and it should contain a series of strings.
|
||||
for val in list {
|
||||
let comp_opt = match val {
|
||||
ValueType::String(Some(comp_id)) => {
|
||||
// Find the comparison
|
||||
comparisons.iter().find(|comp| comp.id == *comp_id)
|
||||
}
|
||||
_ => None
|
||||
};
|
||||
|
||||
let comparison_outcome = match comp_opt {
|
||||
Some(comp) => {
|
||||
// Perform the comparison
|
||||
comp.compare(&student)
|
||||
}
|
||||
_ => Err("".to_string())
|
||||
};
|
||||
|
||||
match comparison_outcome {
|
||||
Ok(true) => {},
|
||||
Ok(false) => final_result = Ok(false),
|
||||
Err(e) => {
|
||||
final_result = Err(e);
|
||||
break;
|
||||
view! {
|
||||
<Panel>
|
||||
<Row>
|
||||
<p>{result_msg}</p>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton on:click=move |_| {
|
||||
let comp_member = "requirements".to_string();
|
||||
let comparison_results = scholarships
|
||||
.iter()
|
||||
.map(|scholarship| {
|
||||
let list = match scholarship.data.get(&comp_member) {
|
||||
Some(ValueType::List(Some(list))) => list,
|
||||
_ => {
|
||||
log!("Cannot find requirements, assuming none.");
|
||||
return Ok(Some(scholarship));
|
||||
}
|
||||
};
|
||||
let mut final_result = Ok(true);
|
||||
for val in list {
|
||||
let comp_opt = match val {
|
||||
ValueType::String(Some(comp_id)) => {
|
||||
comparisons.iter().find(|comp| comp.id == *comp_id)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let comparison_outcome = match comp_opt {
|
||||
Some(comp) => comp.compare(&student.data),
|
||||
_ => Err("".to_string()),
|
||||
};
|
||||
match comparison_outcome {
|
||||
Ok(true) => {}
|
||||
Ok(false) => final_result = Ok(false),
|
||||
Err(e) => {
|
||||
final_result = Err(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
match final_result {
|
||||
Ok(true) => Ok(Some(scholarship)),
|
||||
Ok(false) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Result<Option<&ExpandableInfo>, String>>>();
|
||||
let mut strings = Vec::new();
|
||||
strings.push("Scholarship results: \n".to_string());
|
||||
for result in comparison_results {
|
||||
match result {
|
||||
Ok(Some(scholarship_info)) => {
|
||||
let name_member = "name".to_string();
|
||||
let scl_name = scholarship_info
|
||||
.data
|
||||
.get(&name_member)
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
strings.push(scl_name);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(msg) => strings.push(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match final_result {
|
||||
Ok(true) => Ok(Some(scholarship)),
|
||||
Ok(false) => Ok(None),
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}).collect::<Vec<Result<Option<&ExpandableInfo>, String>>>();
|
||||
|
||||
// Build a display string
|
||||
let mut strings = Vec::new();
|
||||
strings.push("Scholarship results: \n".to_string());
|
||||
for result in comparison_results {
|
||||
match result {
|
||||
Ok(Some(scholarship_info)) => {
|
||||
// Get the scholarship name.
|
||||
let name_member = "name".to_string();
|
||||
let scl_name = scholarship_info.data.get(&name_member).unwrap()
|
||||
.as_string().unwrap().unwrap();
|
||||
strings.push(scl_name);
|
||||
},
|
||||
Ok(None) => {},
|
||||
Err(msg) => strings.push(msg)
|
||||
}
|
||||
}
|
||||
|
||||
let display_text = strings.join("\n").to_string();
|
||||
|
||||
result_msg.set(display_text);
|
||||
}>"Test Comparison"</ActionButton>
|
||||
</Row>
|
||||
</Panel>
|
||||
}),
|
||||
_ => Either::Left(view! {
|
||||
<p>"Failed API requests."</p>
|
||||
})
|
||||
let display_text = strings.join("\n").to_string();
|
||||
result_msg.set(display_text);
|
||||
}>"Test Comparison"</ActionButton>
|
||||
</Row>
|
||||
</Panel>
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => Either::Left(view! { <p>"Failed API requests."</p> }),
|
||||
}
|
||||
}}
|
||||
</Suspense>
|
||||
|
||||
+20
-46
@@ -8,10 +8,12 @@ use std::process::{Command, Stdio};
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::io::Write;
|
||||
|
||||
use crate::common::{ComparisonData, ExpandableInfo};
|
||||
use crate::common::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};
|
||||
use crate::components::{
|
||||
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
|
||||
TextFieldType, ValidatedForm
|
||||
};
|
||||
use crate::input;
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
@@ -22,7 +24,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;
|
||||
use crate::utils::get_user_claims;
|
||||
|
||||
/// # Get Student Info
|
||||
/// Gets a student's information given their `subject`.
|
||||
@@ -156,7 +158,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
async move |subject| get_submission(subject.unwrap_or_default()).await
|
||||
);
|
||||
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
||||
|
||||
|
||||
Effect::new(move || {
|
||||
match server_resource.get() {
|
||||
Some(Ok(submission)) => {
|
||||
@@ -210,28 +212,6 @@ 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>
|
||||
@@ -245,7 +225,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
<Row>
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<ValidatedForm
|
||||
<ValidatedForm
|
||||
on_submit=Callback::new(on_submit)
|
||||
title="Home Page Test Form"
|
||||
description="This really is just a test."
|
||||
@@ -285,7 +265,9 @@ pub fn HomePage() -> impl IntoView {
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=form_data
|
||||
input_type=TextFieldType::Email
|
||||
input_type=TextFieldType::Email(
|
||||
vec!["fakeemail.org".to_string(), "gmail.com".to_string()],
|
||||
)
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
@@ -331,6 +313,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
data_member="town"
|
||||
data_map=form_data
|
||||
required=true
|
||||
other_prompt="Other..."
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -346,26 +329,18 @@ pub fn HomePage() -> impl IntoView {
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Select
|
||||
label="Gender:"
|
||||
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="gender"
|
||||
data_map=form_data
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<MultiEntry
|
||||
label="Testing community involvement:"
|
||||
data_map=form_data
|
||||
data_member="community_involvement"
|
||||
schema=vec![
|
||||
input!(Text, "service_name", "Activity Name:", true, "Some service activity..."),
|
||||
input!(Number, "service_hours", "Total Service Hours", true, "20")
|
||||
input!(
|
||||
Text, "service_name", "Activity Name:", true, "Some service activity..."
|
||||
),
|
||||
input!(
|
||||
Number, "service_hours", "Total Service Hours", true, "20"
|
||||
),
|
||||
]
|
||||
/>
|
||||
</Row>
|
||||
@@ -375,8 +350,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
</Row>
|
||||
}
|
||||
})
|
||||
}}
|
||||
<Row>
|
||||
}} <Row>
|
||||
<ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton>
|
||||
</Row>
|
||||
</Suspense>
|
||||
|
||||
+69
-64
@@ -213,11 +213,7 @@ pub fn LoanerShell() -> impl IntoView {
|
||||
// We'll create the page here - the sidebar contains all the buttons, while the form
|
||||
// container either shows forms or shows placeholder text.
|
||||
view! {
|
||||
<Banner
|
||||
title="Chromebook Loaners"
|
||||
logo="/PHS_Stacked_Acronym.png"
|
||||
path="/loaners"
|
||||
/>
|
||||
<Banner title="Chromebook Loaners" logo="/PHS_Stacked_Acronym.png" path="/loaners" />
|
||||
<div class="flex m-5">
|
||||
<div class="flex flex-1 flex-row gap-3">
|
||||
<div id="sidebar" class="flex flex-1 flex-col gap-2">
|
||||
@@ -247,16 +243,9 @@ pub fn LoanerShell() -> impl IntoView {
|
||||
#[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 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>
|
||||
}
|
||||
}
|
||||
@@ -328,7 +317,7 @@ pub fn LoanerBorrowForm() -> impl IntoView {
|
||||
disabled=elements_disabled
|
||||
data_member="email"
|
||||
data_map=data
|
||||
input_type=TextFieldType::Email
|
||||
input_type=TextFieldType::Email(vec!["region15.org".to_string()])
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
@@ -431,69 +420,85 @@ pub fn LoanerReturnForm() -> impl IntoView {
|
||||
<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>
|
||||
node_ref=dialog_ref
|
||||
>
|
||||
<h2 class="text-2xl font-bold">"Confirmation"</h2>
|
||||
<h4 class="text-xl mb-3">
|
||||
{move || chosen_student.with(|opt_student| {
|
||||
if let Some(student) = opt_student.as_ref() {
|
||||
format!("Are you sure want to check in {} {}?", student.first_name, student.last_name)
|
||||
} else {
|
||||
"There is no student selected.".to_string()
|
||||
}
|
||||
})}
|
||||
{move || {
|
||||
chosen_student
|
||||
.with(|opt_student| {
|
||||
if let Some(student) = opt_student.as_ref() {
|
||||
format!(
|
||||
"Are you sure want to check in {} {}?",
|
||||
student.first_name,
|
||||
student.last_name,
|
||||
)
|
||||
} else {
|
||||
"There is no student selected.".to_string()
|
||||
}
|
||||
})
|
||||
}}
|
||||
</h4>
|
||||
<ActionButton on:click=close_dialog disabled=dialog_disabled>"Close"</ActionButton>
|
||||
<ActionButton on:click=dialog_check_in disabled=dialog_disabled>"Check In"</ActionButton>
|
||||
<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 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()>
|
||||
<Suspense fallback=move || {
|
||||
"Loading...".into_view()
|
||||
}>
|
||||
{move || {
|
||||
match return_list_resource.get() {
|
||||
Some(loaner_list) => {
|
||||
let list_view = loaner_list.iter().map(|student| {
|
||||
let on_click = {
|
||||
let student_click = student.clone();
|
||||
move |_| {
|
||||
log!("Running on_click using student {:?}", student_click);
|
||||
if let Some(dialog) = dialog_ref.get() {
|
||||
chosen_student.set(Some(student_click.clone()));
|
||||
dialog.show_modal().expect("Couldn't show modal dialog.");
|
||||
let list_view = loaner_list
|
||||
.iter()
|
||||
.map(|student| {
|
||||
let on_click = {
|
||||
let student_click = student.clone();
|
||||
move |_| {
|
||||
log!("Running on_click using student {:?}", student_click);
|
||||
if let Some(dialog) = dialog_ref.get() {
|
||||
chosen_student.set(Some(student_click.clone()));
|
||||
dialog.show_modal().expect("Couldn't show modal dialog.");
|
||||
}
|
||||
}
|
||||
};
|
||||
let naive_time = chrono::NaiveDateTime::parse_from_str(
|
||||
&student.date,
|
||||
"%H:%M, %m/%d/%Y",
|
||||
)
|
||||
.unwrap();
|
||||
let time_zone = FixedOffset::west_opt(5 * 3600).unwrap();
|
||||
let student_date = time_zone
|
||||
.from_utc_datetime(&naive_time)
|
||||
.format("%l:%M %p, %m/%d/%Y")
|
||||
.to_string();
|
||||
log!("Found current date {:?}", student_date);
|
||||
|
||||
view! {
|
||||
<div
|
||||
class="flex flex-row p-3 rounded-md cursor-pointer transition-shadow shadow-sm hover:shadow-lg/33"
|
||||
on:click=on_click
|
||||
>
|
||||
<div class="flex-1">
|
||||
{student.first_name.clone()}" "{student.last_name.clone()}
|
||||
</div>
|
||||
<div class="flex-none">{student_date.clone()}</div>
|
||||
</div>
|
||||
}
|
||||
};
|
||||
|
||||
let naive_time = chrono::NaiveDateTime::parse_from_str(&student.date, "%H:%M, %m/%d/%Y")
|
||||
.unwrap();
|
||||
let time_zone = FixedOffset::west_opt(5 * 3600).unwrap();
|
||||
let student_date = time_zone.from_utc_datetime(&naive_time)
|
||||
.format("%l:%M %p, %m/%d/%Y")
|
||||
.to_string();
|
||||
|
||||
log!("Found current date {:?}", student_date);
|
||||
|
||||
view! {
|
||||
<div class="flex flex-row p-3 rounded-md cursor-pointer transition-shadow shadow-sm hover:shadow-lg/33" on:click=on_click>
|
||||
<div class="flex-1">
|
||||
{student.first_name.clone()}" "{student.last_name.clone()}
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
{student_date.clone()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}).collect_view();
|
||||
|
||||
})
|
||||
.collect_view();
|
||||
Either::Left(list_view)
|
||||
},
|
||||
None => Either::Right("No loaners found.".into_view())
|
||||
}
|
||||
None => Either::Right("No loaners found.".into_view()),
|
||||
}
|
||||
}}
|
||||
</Suspense>
|
||||
|
||||
+6
-3
@@ -5,12 +5,15 @@ mod home_page;
|
||||
mod loaner_page;
|
||||
mod provider_portal;
|
||||
mod scholarship_info;
|
||||
mod test_page;
|
||||
mod unauthenticated_page;
|
||||
mod utils;
|
||||
mod provider_contact;
|
||||
mod auth_callback;
|
||||
mod provider_applicants;
|
||||
mod admin;
|
||||
pub(crate) mod student;
|
||||
|
||||
pub use self::{
|
||||
about_page::*, comparison_test::*, home_page::*, loaner_page::*, provider_portal::*,
|
||||
test_page::*, unauthenticated_page::*, scholarship_info::*, provider_contact::*
|
||||
unauthenticated_page::*, scholarship_info::*, provider_contact::*,
|
||||
auth_callback::*, provider_applicants::*, admin::*, student::*,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::Banner;
|
||||
|
||||
#[component]
|
||||
pub fn ApplicantsPageFallback() -> impl IntoView {
|
||||
view! {
|
||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/providers" />
|
||||
<div class="flex flex-col gap-4 mt-3 items-center justify-center">
|
||||
<h1 class="text-3xl font-bold">"Applicants Page"</h1>
|
||||
|
||||
<p>
|
||||
"This page is under construction! It will show all the students that are eligible for your scholarship(s)."
|
||||
</p>
|
||||
<p>"Come back soon!"</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -2,74 +2,10 @@
|
||||
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::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::utils::get_user_claims;
|
||||
use super::api::{get_provider_contact, PutProviderContact};
|
||||
|
||||
#[component]
|
||||
pub fn ProviderContactPage() -> impl IntoView {
|
||||
@@ -117,30 +53,43 @@ pub fn ProviderContactPage() -> impl IntoView {
|
||||
});
|
||||
|
||||
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.")
|
||||
);
|
||||
|
||||
Effect::watch(
|
||||
move || submit_action.value().get(),
|
||||
move |value, _, _| {
|
||||
log!("Running submit_action effect");
|
||||
let Some(result) = value else {
|
||||
return;
|
||||
};
|
||||
|
||||
let toast = match result {
|
||||
Ok(_) => Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.header("Submission Successful")
|
||||
.msg("You can go back or continue editing your responses."),
|
||||
Err(err) => Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.header("Submission Failed")
|
||||
.msg(err.to_string())
|
||||
};
|
||||
|
||||
untrack(move || {
|
||||
submit_action.clear();
|
||||
toasts_context.toast(toast);
|
||||
});
|
||||
});
|
||||
|
||||
// Display contact form.
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Display contact form.
|
||||
view! {
|
||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/" />
|
||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/providers" />
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<Suspense fallback=Loading>
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<ValidatedForm
|
||||
<ValidatedForm
|
||||
on_submit=on_submit
|
||||
title="Contact Form"
|
||||
description="Enter your contact information and click submit."
|
||||
@@ -171,11 +120,11 @@ pub fn ProviderContactPage() -> impl IntoView {
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
placeholder="example@mymailservice.com"
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=contact_info
|
||||
input_type=TextFieldType::Email
|
||||
input_type=TextFieldType::Email(vec!["*".to_string()])
|
||||
required=true
|
||||
/>
|
||||
</Row>
|
||||
@@ -199,17 +148,6 @@ pub fn ProviderContactPage() -> impl IntoView {
|
||||
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>
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
@@ -1,42 +1,67 @@
|
||||
use crate::components::{Banner, DashboardButton};
|
||||
use crate::components::{Banner, DashboardButton, DateList, Loading};
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use crate::pages::api::get_important_dates;
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
|
||||
#[component]
|
||||
pub fn ProviderPortal() -> impl IntoView {
|
||||
view! {
|
||||
<Banner
|
||||
title="Provider Dashboard"
|
||||
logo="PHS_Stacked_Acronym.png"
|
||||
path="/"
|
||||
/>
|
||||
<div class="mx-auto px-6">
|
||||
<div class="container mx-auto px-6 py-8 space-y-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<section class="space-y-4">
|
||||
<DashboardButton
|
||||
title="Profile"
|
||||
description="Edit user profile"
|
||||
icon="/Person_Black.png"
|
||||
path="/providers/profile"
|
||||
/>
|
||||
<DashboardButton
|
||||
title="Create Scholarship"
|
||||
description="Navigate to Scholarship Creation Page"
|
||||
icon="/Create_Black.png"
|
||||
path="/providers/scholarships"
|
||||
/>
|
||||
<DashboardButton
|
||||
title="Applicants"
|
||||
description="View Scholarship Applicants"
|
||||
icon="/Form_Black.png"
|
||||
path="/"
|
||||
/>
|
||||
</section>
|
||||
<section class="space-y-4">
|
||||
let dates_resource = OnceResource::new(async move {
|
||||
get_important_dates().await
|
||||
});
|
||||
|
||||
</section>
|
||||
let dates_list = RwSignal::new(Vec::new());
|
||||
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(mut dates)) = dates_resource.get() {
|
||||
dates.sort_by_key(|info| info.date.get_status());
|
||||
dates_list.set(dates);
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<Banner title="Provider Dashboard" logo="PHS_Stacked_Acronym.png" />
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<div class="mx-auto px-6">
|
||||
<div class="container mx-auto px-6 py-8 space-y-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<section class="space-y-4">
|
||||
<DashboardButton
|
||||
title="Profile"
|
||||
description="Edit user profile"
|
||||
icon="/Person_Black.png"
|
||||
path="/providers/profile"
|
||||
/>
|
||||
<DashboardButton
|
||||
title="Create Scholarship"
|
||||
description="Navigate to Scholarship Creation Page"
|
||||
icon="/Create_Black.png"
|
||||
path="/providers/scholarships"
|
||||
/>
|
||||
<DashboardButton
|
||||
title="Applicants"
|
||||
description="View Scholarship Applicants"
|
||||
icon="/Form_Black.png"
|
||||
path="/providers/applicants"
|
||||
/>
|
||||
</section>
|
||||
<section class="space-y-4">
|
||||
<div class="rounded-lg shadow-lg/25 overflow-hidden">
|
||||
<div class="bg-red-900 text-white px-4 py-3 shadow-lg">
|
||||
<h3 class="text-white font-bold">Important Dates</h3>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<div class="flex flex-col p-3 gap-3">
|
||||
<DateList dates=dates_list />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
}
|
||||
}
|
||||
+180
-140
@@ -1,15 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::either::Either;
|
||||
use leptos::html::Dialog;
|
||||
use leptos::logging::log;
|
||||
use leptos::logging::debug_log;
|
||||
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, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use crate::common::{ComparisonData, ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
||||
use crate::components::{ActionButton, Banner, ChipsList, Header, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use super::UnauthenticatedPage;
|
||||
use crate::pages::utils::get_user_claims;
|
||||
use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized, CreateTestComparisons};
|
||||
use crate::utils::get_user_claims;
|
||||
use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized};
|
||||
|
||||
|
||||
/// # Scholarship Info Page
|
||||
@@ -40,7 +40,7 @@ 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="/providers" />
|
||||
<ToastList>
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
@@ -111,7 +111,7 @@ fn ScholarshipList(
|
||||
(refresh_token.get(), provider_id.get())
|
||||
},
|
||||
|(_, provider_id)| async move {
|
||||
log!("Fetching from scholarship API.");
|
||||
debug_log!("Fetching from scholarship API.");
|
||||
let Some(provider_id) = provider_id else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
@@ -128,8 +128,7 @@ fn ScholarshipList(
|
||||
// Button handlers
|
||||
let create_on_click = move |_| {
|
||||
create_action.dispatch(RegisterScholarship {
|
||||
provider_id: provider_id.get().unwrap(),
|
||||
scholarship_name: "Testing Scholarship".to_string()
|
||||
provider_id: provider_id.get().unwrap()
|
||||
});
|
||||
};
|
||||
|
||||
@@ -175,10 +174,27 @@ fn ScholarshipList(
|
||||
}
|
||||
});
|
||||
|
||||
let modal_on_click_continue = move |_| {
|
||||
let subject = pending_delete
|
||||
.get()
|
||||
.map(|s| s.subject.clone())
|
||||
.unwrap_or_default();
|
||||
debug_log!("Deleting scholarship with subject {}", subject);
|
||||
delete_action
|
||||
.dispatch(DeleteProviderScholarship {
|
||||
scholarship_id: subject,
|
||||
provider_id: provider_id.get().unwrap_or_default(),
|
||||
});
|
||||
};
|
||||
|
||||
view! {
|
||||
<dialog node_ref=delete_dialog_ref class="m-auto p-2">
|
||||
<dialog
|
||||
node_ref=delete_dialog_ref
|
||||
class="m-auto p-5 rounded-lg shadow-xl/50
|
||||
backdrop:backdrop-blur-xs backdrop:transition-backdrop-filter"
|
||||
>
|
||||
<h2 class="text-2xl font-bold">"Confirm Deletion"</h2>
|
||||
<p>
|
||||
<p class="mb-3">
|
||||
"Are you sure you want to delete "
|
||||
{move || {
|
||||
pending_delete
|
||||
@@ -189,28 +205,16 @@ fn ScholarshipList(
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or("<no name found>".to_string())
|
||||
.unwrap_or("this unnamed scholarship".to_string())
|
||||
})
|
||||
}} "?"
|
||||
</p>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
let subject = pending_delete
|
||||
.get()
|
||||
.map(|s| s.subject.clone())
|
||||
.unwrap_or_default();
|
||||
log!("Deleting scholarship with subject {}", subject);
|
||||
delete_action
|
||||
.dispatch(DeleteProviderScholarship {
|
||||
scholarship_id: subject,
|
||||
provider_id: provider_id.get().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
disabled=modal_disabled
|
||||
>
|
||||
"Confirm"
|
||||
</ActionButton>
|
||||
<ActionButton on:click=move |_| pending_delete.set(None)>"Cancel"</ActionButton>
|
||||
<div class="flex mx-auto items-center justify-center">
|
||||
<ActionButton on:click=modal_on_click_continue disabled=modal_disabled>
|
||||
"Yes"
|
||||
</ActionButton>
|
||||
<ActionButton on:click=move |_| pending_delete.set(None)>"No"</ActionButton>
|
||||
</div>
|
||||
</dialog>
|
||||
<div class="w-75">
|
||||
<Panel>
|
||||
@@ -298,7 +302,7 @@ fn ScholarshipListEntry(
|
||||
let name = scholarship.data.get("name")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string().unwrap_or_default()
|
||||
.unwrap_or("<no name found>".to_string());
|
||||
.unwrap_or("Unnamed Scholarship".to_string());
|
||||
|
||||
// Button click handlers
|
||||
let edit_click = {
|
||||
@@ -361,42 +365,7 @@ fn ScholarshipForm(
|
||||
/// submission fails, this function is not run.
|
||||
#[prop(into)] on_submit: Callback<()>
|
||||
) -> impl IntoView {
|
||||
let submit_action = ServerAction::<CreateScholarshipInfo>::new();
|
||||
|
||||
let submit_status = RwSignal::new(SubmitStatus::Idle);
|
||||
let elements_disabled = Signal::derive(move || {
|
||||
matches!(submit_action.pending().get(), true)
|
||||
});
|
||||
|
||||
Effect::new(move || {
|
||||
if submit_action.pending().get() {
|
||||
submit_status.set(SubmitStatus::Sending);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(result) = submit_action.value().get() {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
submit_status.set(SubmitStatus::Success);
|
||||
on_submit.run(());
|
||||
},
|
||||
Err(_err) => submit_status.set(SubmitStatus::Error(()))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let comparison_lists = Resource::new(
|
||||
move || scholarship_id.get().is_some(),
|
||||
async move |_| {
|
||||
get_comparisons_categorized().await
|
||||
}
|
||||
);
|
||||
|
||||
Effect::new(move || {
|
||||
if let Some(Ok(map)) = comparison_lists.get() {
|
||||
log!("Available categories: {:?}", map.keys());
|
||||
}
|
||||
});
|
||||
//#region Form Data Setup
|
||||
|
||||
let scholarship_info = Resource::new(
|
||||
move || scholarship_id.get(),
|
||||
@@ -416,8 +385,10 @@ fn ScholarshipForm(
|
||||
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") {
|
||||
debug_log!("Found requirements map: {:?}", map);
|
||||
map.clone()
|
||||
} else {
|
||||
debug_log!("Couldn't find requirements map in scholarship info. Using empty map.");
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
@@ -426,12 +397,36 @@ fn ScholarshipForm(
|
||||
}
|
||||
});
|
||||
|
||||
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()
|
||||
//#endregion
|
||||
//#region Comparison Logic
|
||||
|
||||
let comparison_lists = Resource::new(
|
||||
move || scholarship_id.get().is_some(),
|
||||
async move |_| {
|
||||
get_comparisons_categorized().await
|
||||
}
|
||||
);
|
||||
|
||||
let comparison_lists_sorted = Signal::derive(move || {
|
||||
if let Some(Ok(map)) = comparison_lists.get() {
|
||||
let mut map = map.clone();
|
||||
map
|
||||
.into_iter()
|
||||
.map(|(cat, mut list)| {
|
||||
list.sort_by_key(|comp| comp.display_text.clone());
|
||||
(cat, list)
|
||||
})
|
||||
.collect::<HashMap<String, Vec<ComparisonData>>>()
|
||||
} else {
|
||||
HashMap::new()
|
||||
}
|
||||
});
|
||||
|
||||
let get_comp_category = move |category: &str| {
|
||||
comparison_lists_sorted.get()
|
||||
.get(category)
|
||||
.cloned()
|
||||
.unwrap_or(Vec::new())
|
||||
};
|
||||
|
||||
let get_comp_ids = move |category: &str| {
|
||||
@@ -464,37 +459,77 @@ fn ScholarshipForm(
|
||||
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 major_ids = Signal::derive(move || get_comp_ids("Majors"));
|
||||
let major_text = Signal::derive(move || get_comp_text("Majors"));
|
||||
|
||||
//#endregion
|
||||
//#region Submission Logic
|
||||
|
||||
let submit_action = ServerAction::<CreateScholarshipInfo>::new();
|
||||
|
||||
let submit_status = RwSignal::new(SubmitStatus::Idle);
|
||||
let elements_disabled = Signal::derive(move || {
|
||||
matches!(submit_action.pending().get(), true)
|
||||
});
|
||||
|
||||
Effect::new(move || {
|
||||
if submit_action.pending().get() {
|
||||
submit_status.set(SubmitStatus::Sending);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(result) = submit_action.value().get() {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
submit_status.set(SubmitStatus::Success);
|
||||
on_submit.run(());
|
||||
},
|
||||
Err(_err) => submit_status.set(SubmitStatus::Error(()))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
info.data.insert("requirements".to_string(), ValueType::Map(Some(chips_data.get())));
|
||||
|
||||
log!("Map values: {:?}", info.data);
|
||||
debug_log!("Map values: {:?}", info.data);
|
||||
submit_action.dispatch(CreateScholarshipInfo {
|
||||
info
|
||||
});
|
||||
};
|
||||
|
||||
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();
|
||||
Effect::watch(
|
||||
move || submit_action.value().get(),
|
||||
move |value, _, _| {
|
||||
debug_log!("Running submit_action effect");
|
||||
let Some(result) = value else {
|
||||
return;
|
||||
};
|
||||
|
||||
let toast = match result {
|
||||
Ok(_) => Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.header("Submission Successful")
|
||||
.msg("You can go back or continue editing your responses."),
|
||||
Err(err) => Toast::new()
|
||||
.id(uuid::Uuid::new_v4())
|
||||
.header("Submission Failed")
|
||||
.msg(err.to_string())
|
||||
};
|
||||
|
||||
untrack(move || {
|
||||
submit_action.clear();
|
||||
toasts.toast(toast);
|
||||
});
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
//#endregion
|
||||
|
||||
view! {
|
||||
<Panel>
|
||||
@@ -505,24 +540,19 @@ fn ScholarshipForm(
|
||||
}
|
||||
>
|
||||
<Suspense fallback=Loading>
|
||||
<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()
|
||||
.map(|res_scholarship| {
|
||||
match res_scholarship {
|
||||
Ok(_) => {
|
||||
Either::Left(
|
||||
view! {
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
"Scholarship Info Form (test)"
|
||||
</h1>
|
||||
</Row>
|
||||
{move || {
|
||||
scholarship_info
|
||||
.get()
|
||||
.map(|res_scholarship| {
|
||||
match res_scholarship {
|
||||
Ok(_) => {
|
||||
Either::Left(
|
||||
view! {
|
||||
<ValidatedForm
|
||||
on_submit=Callback::new(on_submit)
|
||||
title="Scholarship Info Form"
|
||||
description="Create or edit a scholarship. Input all information and click Submit."
|
||||
>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Scholarship Name"
|
||||
@@ -584,32 +614,33 @@ fn ScholarshipForm(
|
||||
<RadioList
|
||||
data_member="award_to"
|
||||
data_map=form_data
|
||||
items=vec!["School".to_string(), "Student".to_string()]
|
||||
items=vec!["University".to_string(), "Student".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Will the award be made to the school or the student?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="essay_required"
|
||||
data_map=form_data
|
||||
items=vec!["Yes".to_string(), "No".to_string()]
|
||||
disabled=elements_disabled
|
||||
label="Do you require a student essay?"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
data_member="essay_prompt"
|
||||
data_map=form_data
|
||||
items=vec!["Test 1", "Test 2", "Test 3"]
|
||||
items=vec![
|
||||
"Write an essay describing your major/career direction. Include any abilities, interests, experiences, employment or clubs you have had that have affected your direction. If you are undecided, indicate why.",
|
||||
"Why is community service important? How has it personally affected you and your goals and what role do you hope to play in it in the future?",
|
||||
"How has participation in sports been beneficial to your personal, academic, and emotional growth?",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.map(|prompt| prompt.to_string())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
label="If so, select a prompt from the list below."
|
||||
label="Select an essay prompt, or leave blank if none is required:"
|
||||
other_prompt="Custom prompt..."
|
||||
/>
|
||||
</Row>
|
||||
<Header
|
||||
title="Eligibility Requirements"
|
||||
description="Choose any requirements that this scholarship may have. If there are no extra requirements,
|
||||
leave all options blank, however please note that this will apply all students to your scholarship."
|
||||
/>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="GPA Requirements"
|
||||
@@ -617,11 +648,12 @@ fn ScholarshipForm(
|
||||
data_map=chips_data
|
||||
values=gpa_ids
|
||||
displayed_text=gpa_text
|
||||
allows_multiple=false
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Sports Participation"
|
||||
label="Sports and Activities"
|
||||
data_member="sports_participation"
|
||||
data_map=chips_data
|
||||
values=sports_ids
|
||||
@@ -635,6 +667,7 @@ fn ScholarshipForm(
|
||||
data_map=chips_data
|
||||
values=comm_ids
|
||||
displayed_text=comm_text
|
||||
allows_multiple=false
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -644,6 +677,16 @@ fn ScholarshipForm(
|
||||
data_map=chips_data
|
||||
values=res_ids
|
||||
displayed_text=res_text
|
||||
allows_multiple=false
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Majors"
|
||||
data_member="major"
|
||||
data_map=chips_data
|
||||
values=major_ids
|
||||
displayed_text=major_text
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -655,24 +698,21 @@ fn ScholarshipForm(
|
||||
displayed_text=misc_text
|
||||
/>
|
||||
</Row>
|
||||
},
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
Either::Right(
|
||||
view! {
|
||||
<p>"Error while getting scholarship: "{err.to_string()}</p>
|
||||
},
|
||||
)
|
||||
}
|
||||
</ValidatedForm>
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
</ValidatedForm>
|
||||
<ActionButton on:click=move |_| {
|
||||
create_comps.dispatch(CreateTestComparisons {});
|
||||
}>"Create Comparisons"</ActionButton>
|
||||
Err(err) => {
|
||||
Either::Right(
|
||||
view! {
|
||||
<p>"Error while getting scholarship: "{err.to_string()}</p>
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect_view()
|
||||
}}
|
||||
</Suspense>
|
||||
</Show>
|
||||
</Panel>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::{Loading, OutlinedTextField, TextFieldType, ValidatedForm};
|
||||
use crate::pages::student::form_setup::use_student_form;
|
||||
|
||||
#[component]
|
||||
pub fn StudentAcademicsPage() -> impl IntoView {
|
||||
let controller = use_student_form("academics");
|
||||
|
||||
view! {
|
||||
<Show
|
||||
when=move || controller.data_resource.get().is_some()
|
||||
fallback=Loading
|
||||
>
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm title="Academic Information" on_submit=controller.submit_action>
|
||||
<OutlinedTextField
|
||||
label="Unweighted GPA:"
|
||||
data_map=controller.data_map
|
||||
data_member="unweighted_gpa"
|
||||
placeholder="4.0"
|
||||
required=true
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Weighted GPA:"
|
||||
data_map=controller.data_map
|
||||
data_member="weighted_gpa"
|
||||
placeholder="4.5"
|
||||
required=true
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Highest SAT Score:"
|
||||
data_map=controller.data_map
|
||||
data_member="sat_score"
|
||||
placeholder="1600"
|
||||
required=false
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Highest ACT Score:"
|
||||
data_map=controller.data_map
|
||||
data_member="act_score"
|
||||
placeholder="36"
|
||||
required=false
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Academic Honors:"
|
||||
data_map=controller.data_map
|
||||
data_member="academic_honors"
|
||||
placeholder="TBD"
|
||||
required=false
|
||||
input_type=TextFieldType::Text
|
||||
/>
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
<div class="flex flex-1" />
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn AdditionalPage() -> impl IntoView {
|
||||
view! {
|
||||
<div class="flex flex-1 flex-col gap-4 items-center m-4">
|
||||
<div class="font-bold text-3xl">"Student Additional Forms"</div>
|
||||
<div class="text-lg">
|
||||
"This is just a placeholder for now. We'll show redirect buttons to each form."
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::{Loading, MultiEntry, ValidatedForm};
|
||||
use crate::input;
|
||||
use crate::pages::student::form_setup::use_student_form;
|
||||
|
||||
#[component]
|
||||
pub fn StudentAthleticsPage() -> impl IntoView {
|
||||
let controller = use_student_form("athletics");
|
||||
|
||||
view! {
|
||||
<Show
|
||||
when=move || controller.data_resource.get().is_some()
|
||||
fallback=Loading
|
||||
>
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm title="Athletics Information" on_submit=controller.submit_action>
|
||||
<MultiEntry
|
||||
label="Athletics Activities:"
|
||||
description="Leave blank if not applicable."
|
||||
data_map=controller.data_map
|
||||
data_member="sports_participation"
|
||||
schema=vec![
|
||||
input!(
|
||||
Select, "sport_name", "Sport Name:", true, [
|
||||
"Football",
|
||||
"Soccer",
|
||||
"Cheerleading",
|
||||
"Field Hockey",
|
||||
"Swimming",
|
||||
"Golf",
|
||||
"Basketball",
|
||||
"Track",
|
||||
"Gymnastics",
|
||||
"Ice Hockey",
|
||||
"Ski",
|
||||
"Wrestling",
|
||||
"Lacrosse",
|
||||
"Softball",
|
||||
"Tennis",
|
||||
"Baseball"
|
||||
]
|
||||
),
|
||||
input!(
|
||||
Checkbox, "grades", "Grades Participated:", true, [
|
||||
"9th",
|
||||
"10th",
|
||||
"11th",
|
||||
"12th"
|
||||
]
|
||||
),
|
||||
input!(
|
||||
Text, "achievements", "Special Achievements:", false, "Example 1, Example 2, etc..."
|
||||
),
|
||||
]
|
||||
/>
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
<div class="flex flex-1" />
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::{Loading, OutlinedTextField, Select, TextFieldType, ValidatedForm};
|
||||
use crate::pages::student::form_setup::use_student_form;
|
||||
|
||||
#[component]
|
||||
pub fn StudentDemographicsPage() -> impl IntoView {
|
||||
// This page should show a form that the students need to fill out.
|
||||
// It's a simple form, really nothing more than a contact form.
|
||||
// I'm going to test out creating this form without using a panel - we'll just use the page as
|
||||
// it is with a specially spaced invisible div instead of a shaded container.
|
||||
|
||||
let controller = use_student_form("demographics");
|
||||
|
||||
view! {
|
||||
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm title="Student Demographic Form" on_submit=controller.submit_action>
|
||||
// Inputs here.
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
data_map=controller.data_map
|
||||
data_member="first_name"
|
||||
placeholder="John"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
data_map=controller.data_map
|
||||
data_member="last_name"
|
||||
placeholder="Smith"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
// There must be a date of birth selection field here.
|
||||
<OutlinedTextField
|
||||
label="Student ID Number:"
|
||||
data_map=controller.data_map
|
||||
data_member="id_number"
|
||||
placeholder="Your 7 digit number..."
|
||||
input_type=TextFieldType::Number
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Preferred Email:"
|
||||
data_map=controller.data_map
|
||||
data_member="email"
|
||||
placeholder="me@example.com"
|
||||
input_type=TextFieldType::Email(vec!["*".to_string()])
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Preferred Phone Number:"
|
||||
data_map=controller.data_map
|
||||
data_member="phone_number"
|
||||
placeholder="000-000-0000"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Street Address:"
|
||||
data_map=controller.data_map
|
||||
data_member="street_address"
|
||||
placeholder="234 Judd Rd"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
<Select
|
||||
label="Town:"
|
||||
value_list=vec!["Southbury".to_string(), "Middlebury".to_string()]
|
||||
data_map=controller.data_map
|
||||
data_member="town"
|
||||
required=true
|
||||
/>
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
<div class="flex flex-1" />
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
use leptos::logging::debug_log;
|
||||
use leptos::prelude::*;
|
||||
use crate::common::{ComparisonData, ExpandableInfo, ValueType};
|
||||
use crate::pages::api::{get_all_scholarship_info, get_comparison_info};
|
||||
use crate::pages::api::students::get_all_student_data;
|
||||
use crate::utils::get_user_claims;
|
||||
|
||||
#[component]
|
||||
pub fn StudentEligibilityPage() -> impl IntoView {
|
||||
// This page will show all scholarships that the student is eligible for.
|
||||
// The scholarships are obviously pulled using a readonly API, and we'll also need to resolve
|
||||
// all of the relation IDs that are stored inside of it.
|
||||
// Then, for each scholarship found, we'll check every relation and filter out scholarships that
|
||||
// do not pass the check. We'll show some sort of simple list to indicate what scholarships passed
|
||||
// the check, and another to indicate which ones didn't.
|
||||
|
||||
// My only reservation with showing the scholarships that aren't valid is that people will want
|
||||
// to know why. That's hard information to keep track of right now.
|
||||
|
||||
let user_claims = get_user_claims();
|
||||
let user_id = Memo::new(move |_| {
|
||||
user_claims.get()
|
||||
.map(|info| info.claims.subject.clone())
|
||||
});
|
||||
|
||||
let refresh_trigger = Trigger::new();
|
||||
|
||||
let scholarships_resource = Resource::new(
|
||||
move || refresh_trigger.track(),
|
||||
async move |_| {
|
||||
get_all_scholarship_info().await
|
||||
}
|
||||
);
|
||||
let relations_resource = Resource::new(
|
||||
move || refresh_trigger.track(),
|
||||
async move |_| {
|
||||
get_comparison_info().await
|
||||
}
|
||||
);
|
||||
let student_resource = Resource::new(
|
||||
move || (user_id.get().unwrap_or_default(), refresh_trigger.track()),
|
||||
async move |(user_id, _)| {
|
||||
get_all_student_data(user_id).await
|
||||
}
|
||||
);
|
||||
|
||||
view! {
|
||||
<div class="flex-1" />
|
||||
<div>
|
||||
<Suspense fallback=move || {
|
||||
view! { <div>"Checking eligibility..."</div> }
|
||||
}>
|
||||
{move || {
|
||||
let Some(Ok(scholarships_list)) = scholarships_resource.get() else {
|
||||
return view! {}.into_any();
|
||||
};
|
||||
let Some(Ok(relations_list)) = relations_resource.get() else {
|
||||
return view! {}.into_any();
|
||||
};
|
||||
let Some(Ok(student_info)) = student_resource.get() else {
|
||||
return view! {}.into_any();
|
||||
};
|
||||
|
||||
let valid_list = scholarships_list.iter()
|
||||
.filter_map(|scholarship| {
|
||||
let scholarship_name = scholarship.data.get("name")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string().ok().flatten().unwrap_or_default();
|
||||
debug_log!("Checking scholarship: {}", scholarship_name);
|
||||
|
||||
// Each scholarship will return Some(itself), or None. This is will be automatically
|
||||
// filtered based on the information. Invalid scholarships are None, along with those
|
||||
// that fail validation for any reason. Scholarships that are valid or did not choose
|
||||
// any requirements are Some(scholarship).
|
||||
let requirements_map = scholarship.data.get("requirements")
|
||||
.unwrap_or(&ValueType::Map(None))
|
||||
.as_map().ok().flatten()
|
||||
.unwrap_or_default()
|
||||
.values().cloned()
|
||||
.collect::<Vec<ValueType>>();
|
||||
|
||||
let resolved_requirements = requirements_map
|
||||
.iter()
|
||||
.map(|list_val| {
|
||||
// We want to cast to a list, and then map the list to a new
|
||||
// one that contains the correct ComparisonData.
|
||||
let list = list_val.as_list().ok().flatten().unwrap_or_default();
|
||||
list.iter().filter_map(|v| {
|
||||
let id_string = v.as_string().ok().flatten().unwrap_or_default();
|
||||
relations_list.iter()
|
||||
.find(|relation| relation.id == id_string)
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<ComparisonData>>()
|
||||
})
|
||||
.collect::<Vec<Vec<ComparisonData>>>();
|
||||
|
||||
debug_log!("Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>());
|
||||
|
||||
let valid = resolved_requirements.iter()
|
||||
.all(|list| {
|
||||
list.iter().fold(false, |prev, requirement| {
|
||||
let result = requirement.compare(&student_info)
|
||||
.unwrap_or(false);
|
||||
|
||||
if prev { prev } else { result }
|
||||
})
|
||||
});
|
||||
|
||||
if valid {
|
||||
debug_log!("Scholarship is valid.");
|
||||
Some(scholarship.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<ExpandableInfo>>();
|
||||
|
||||
view! {
|
||||
<div>{format!("Valid scholarships: {}", valid_list.len())}</div>
|
||||
<For
|
||||
each=move || valid_list.clone()
|
||||
key=|scholarship| {
|
||||
scholarship
|
||||
.subject
|
||||
.clone()
|
||||
}
|
||||
children=move |scholarship| {
|
||||
let scholarship = StoredValue::new(scholarship);
|
||||
let scholarship_name = Memo::new(move |_| {
|
||||
scholarship
|
||||
.read_value()
|
||||
.data
|
||||
.get("name")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let scholarship_essay_prompt = Memo::new(move |_| {
|
||||
scholarship
|
||||
.read_value()
|
||||
.data
|
||||
.get("essay_prompt")
|
||||
.unwrap_or(&ValueType::String(None))
|
||||
.as_string()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let essay_required = Memo::new(move |_| {
|
||||
!scholarship_essay_prompt.get().is_empty()
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="flex flex-1 flex-row gap-2 p-3">
|
||||
<div>{scholarship_name}</div>
|
||||
<Show when=move || essay_required.get()>
|
||||
<div>{scholarship_essay_prompt}</div>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
/>
|
||||
}.into_any()
|
||||
}}
|
||||
</Suspense>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::{Loading, MultiEntry, OutlinedTextField, TextFieldType, ValidatedForm};
|
||||
use crate::input;
|
||||
use crate::pages::student::form_setup::use_student_form;
|
||||
|
||||
#[component]
|
||||
pub fn StudentExtracurricularsPage() -> impl IntoView {
|
||||
let controller = use_student_form("demographics");
|
||||
|
||||
view! {
|
||||
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm
|
||||
title="Extracurricular Information"
|
||||
on_submit=controller.submit_action
|
||||
>
|
||||
// Inputs here.
|
||||
<OutlinedTextField
|
||||
label="Total number of service hours:"
|
||||
data_map=controller.data_map
|
||||
data_member="service_hours"
|
||||
placeholder="Any number..."
|
||||
input_type=TextFieldType::Number
|
||||
required=true
|
||||
/>
|
||||
<MultiEntry
|
||||
label="Extracurricular Activities:"
|
||||
description="Leave blank if not applicable."
|
||||
data_map=controller.data_map
|
||||
data_member="extracurricular"
|
||||
schema=vec![
|
||||
input!(
|
||||
Text, "activity_name", "Activity Name:", true, "Some activity..."
|
||||
),
|
||||
input!(Number, "num_hours", "Number of hours completed:", true, "40"),
|
||||
input!(Number, "num_weeks", "Number of weeks participated:", true, "3"),
|
||||
input!(
|
||||
Text, "special_involvement", "Any special involvement:", false, ""
|
||||
),
|
||||
input!(
|
||||
Checkbox, "grades", "Grades Participated:", true, ["9th", "10th", "11th", "12th"]
|
||||
),
|
||||
]
|
||||
/>
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
<div class="flex flex-1" />
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::{Loading, OutlinedTextField, TextFieldType, ValidatedForm};
|
||||
use crate::pages::student::form_setup::use_student_form;
|
||||
|
||||
#[component]
|
||||
pub fn StudentFamilyPage() -> impl IntoView {
|
||||
let controller = use_student_form("family");
|
||||
|
||||
view! {
|
||||
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm title="Family Information" on_submit=controller.submit_action>
|
||||
// Inputs here.
|
||||
<OutlinedTextField
|
||||
label="Total number of children in family:"
|
||||
data_map=controller.data_map
|
||||
data_member="num_children"
|
||||
placeholder="Any number..."
|
||||
input_type=TextFieldType::Number
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Total number of children currently attending college:"
|
||||
data_map=controller.data_map
|
||||
data_member="num_children_college"
|
||||
placeholder="Any number..."
|
||||
input_type=TextFieldType::Number
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Parent/Guardian 1 Name:"
|
||||
data_map=controller.data_map
|
||||
data_member="parent_one_name"
|
||||
placeholder="John Smith"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
// TODO Could this be a select dropdown?
|
||||
<OutlinedTextField
|
||||
label="Parent/Guardian 1 Relationship:"
|
||||
data_map=controller.data_map
|
||||
data_member="parent_one_relationship"
|
||||
placeholder="Mother/Father"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
// TODO Get a better example
|
||||
<OutlinedTextField
|
||||
label="Parent/Guardian 1 Occupation:"
|
||||
data_map=controller.data_map
|
||||
data_member="parent_one_occupation"
|
||||
placeholder="Milkman"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Parent/Guardian 1 Employer:"
|
||||
data_map=controller.data_map
|
||||
data_member="parent_one_employer"
|
||||
placeholder="Example Employer"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
|
||||
<OutlinedTextField
|
||||
label="Parent/Guardian 2 Name:"
|
||||
data_map=controller.data_map
|
||||
data_member="parent_two_name"
|
||||
placeholder="John Smith"
|
||||
input_type=TextFieldType::Text
|
||||
required=false
|
||||
/>
|
||||
// TODO Could this be a select dropdown?
|
||||
<OutlinedTextField
|
||||
label="Parent/Guardian 2 Relationship:"
|
||||
data_map=controller.data_map
|
||||
data_member="parent_two_relationship"
|
||||
placeholder="Mother/Father"
|
||||
input_type=TextFieldType::Text
|
||||
required=false
|
||||
/>
|
||||
// TODO Get a better example
|
||||
<OutlinedTextField
|
||||
label="Parent/Guardian 2 Occupation:"
|
||||
data_map=controller.data_map
|
||||
data_member="parent_two_occupation"
|
||||
placeholder="Milkman"
|
||||
input_type=TextFieldType::Text
|
||||
required=false
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Parent/Guardian 2 Employer:"
|
||||
data_map=controller.data_map
|
||||
data_member="parent_two_employer"
|
||||
placeholder="Example Employer"
|
||||
input_type=TextFieldType::Text
|
||||
required=false
|
||||
/>
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
<div class="flex flex-1" />
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::prelude::*;
|
||||
use crate::common::ValueType;
|
||||
use crate::pages::api::students::{get_student_data, PutStudentData};
|
||||
use crate::utils::get_user_claims;
|
||||
|
||||
pub struct StudentFormInfo {
|
||||
pub data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
pub user_id: Memo<Option<String>>,
|
||||
pub submit_action: Callback<()>,
|
||||
pub refresh_trigger: Trigger,
|
||||
pub data_resource: Resource<Result<HashMap<String, ValueType>, ServerFnError>>
|
||||
}
|
||||
|
||||
pub fn use_student_form(
|
||||
form_type: impl Into<Signal<String>>
|
||||
) -> StudentFormInfo {
|
||||
let form_type = form_type.into();
|
||||
|
||||
let user_claims = get_user_claims();
|
||||
let user_id = Memo::new(move |_| {
|
||||
user_claims.get()
|
||||
.map(|info| info.claims.subject.clone())
|
||||
});
|
||||
|
||||
let data_map = RwSignal::new(HashMap::new());
|
||||
|
||||
let refresh_trigger = Trigger::new();
|
||||
let data_resource = Resource::new(
|
||||
move || (user_id.get().unwrap_or_default(), form_type.get(), refresh_trigger.track()),
|
||||
async move |(id, form_type, _)| get_student_data(
|
||||
id,
|
||||
form_type,
|
||||
).await
|
||||
);
|
||||
|
||||
Effect::new(move || {
|
||||
data_resource.with(|map_opt| {
|
||||
let Some(Ok(map)) = map_opt else {
|
||||
return;
|
||||
};
|
||||
|
||||
data_map.set(map.to_owned());
|
||||
});
|
||||
});
|
||||
|
||||
let submit_action = ServerAction::<PutStudentData>::new();
|
||||
let on_submit = move || {
|
||||
submit_action.dispatch(PutStudentData {
|
||||
subject: user_id.get().unwrap_or_default(),
|
||||
data_type: form_type.get(),
|
||||
data_map: data_map.get()
|
||||
});
|
||||
};
|
||||
|
||||
StudentFormInfo {
|
||||
data_map,
|
||||
refresh_trigger,
|
||||
data_resource,
|
||||
user_id,
|
||||
submit_action: on_submit.into(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use crate::common::ValueType;
|
||||
use crate::components::ActionButton;
|
||||
use crate::pages::api::students::{GetStudentData, PutStudentData};
|
||||
use crate::utils::get_user_claims;
|
||||
|
||||
#[component]
|
||||
pub fn StudentHomePage() -> impl IntoView {
|
||||
let user_claims = get_user_claims();
|
||||
let user_id = Memo::new(move |_| {
|
||||
user_claims.get()
|
||||
.map(|info| info.claims.subject.clone())
|
||||
});
|
||||
|
||||
let create_student_data = ServerAction::<PutStudentData>::new();
|
||||
let get_student_data = ServerAction::<GetStudentData>::new();
|
||||
|
||||
let on_click_student_test = move |_| {
|
||||
create_student_data.dispatch(PutStudentData {
|
||||
subject: user_id.get().unwrap_or_default(),
|
||||
data_type: "TESTING".into(),
|
||||
data_map: HashMap::from([
|
||||
("testing_info".into(), ValueType::String(Some("hello!".to_string())))
|
||||
]),
|
||||
});
|
||||
};
|
||||
|
||||
let on_click_get_student = move |_| {
|
||||
get_student_data.dispatch(GetStudentData {
|
||||
subject: user_id.get().unwrap_or_default(),
|
||||
data_type: "TESTING".into()
|
||||
});
|
||||
};
|
||||
|
||||
Effect::new(move || {
|
||||
let Some(value) = get_student_data.value().get() else {
|
||||
return;
|
||||
};
|
||||
|
||||
log!("{:?}", value);
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="flex flex-1 flex-col gap-4 items-center m-4">
|
||||
<div class="font-bold text-3xl">"Student Home Page"</div>
|
||||
<div class="text-lg">
|
||||
"This is just a placeholder for now. We'll show some redirect buttons."
|
||||
</div>
|
||||
<ActionButton on:click=on_click_student_test>"Test Put Student Function"</ActionButton>
|
||||
<ActionButton on:click=on_click_get_student>"Test Get Student Function"</ActionButton>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
mod shell;
|
||||
mod home_page;
|
||||
mod demographics_page;
|
||||
mod additional_page;
|
||||
mod athletics_page;
|
||||
mod academics_page;
|
||||
mod extracurriculars_page;
|
||||
mod work_experience_page;
|
||||
mod university_page;
|
||||
mod family_info_page;
|
||||
mod form_setup;
|
||||
mod eligibility_page;
|
||||
|
||||
pub use shell::*;
|
||||
pub use home_page::*;
|
||||
pub use demographics_page::*;
|
||||
pub use additional_page::*;
|
||||
pub use athletics_page::*;
|
||||
pub use academics_page::*;
|
||||
pub use extracurriculars_page::*;
|
||||
pub use work_experience_page::*;
|
||||
pub use university_page::*;
|
||||
pub use family_info_page::*;
|
||||
pub use eligibility_page::*;
|
||||
@@ -0,0 +1,50 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use crate::common::{SubTabInfo, TabInfo};
|
||||
use crate::components::{Banner, Loading, TabSidebarList};
|
||||
use crate::components::login::StudentLoginContext;
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
|
||||
#[component]
|
||||
pub fn StudentShell() -> impl IntoView {
|
||||
// Create login context
|
||||
view! {
|
||||
<StudentLoginContext>
|
||||
<div class="h-screen flex flex-col overflow-hidden">
|
||||
<Banner
|
||||
title="R15 Student Scholarship Application"
|
||||
logo="/PHS_Stacked_Acronym.png"
|
||||
/>
|
||||
|
||||
<div class="flex flex-1 min-h-0">
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
<TabSidebarList
|
||||
base_path="students"
|
||||
tabs=vec![
|
||||
TabInfo::new("Home", "home", None),
|
||||
TabInfo::new("Demographics", "demographics", None),
|
||||
TabInfo::new(
|
||||
"Additional Info",
|
||||
"additional",
|
||||
Some(
|
||||
vec![
|
||||
SubTabInfo::new("Academics", "academics"),
|
||||
SubTabInfo::new("Athletics", "athletics"),
|
||||
SubTabInfo::new("Work Experience", "work-experience"),
|
||||
SubTabInfo::new("Extracurriculars", "extracurriculars"),
|
||||
SubTabInfo::new("University Info", "university"),
|
||||
SubTabInfo::new("Family Info", "family-info"),
|
||||
],
|
||||
),
|
||||
),
|
||||
TabInfo::new("Eligible Scholarships", "eligibility", None)
|
||||
]
|
||||
/>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
</div>
|
||||
</div>
|
||||
</StudentLoginContext>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::{Loading, OutlinedTextField, Select, TextFieldType, ValidatedForm};
|
||||
use crate::pages::student::form_setup::use_student_form;
|
||||
|
||||
#[component]
|
||||
pub fn StudentUniversityPage() -> impl IntoView {
|
||||
let controller = use_student_form("university");
|
||||
|
||||
// college name, state, city, zip
|
||||
// major, study field, intended career, college_acceptance
|
||||
|
||||
view! {
|
||||
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm title="University Information" on_submit=controller.submit_action>
|
||||
// Inputs here.
|
||||
<OutlinedTextField
|
||||
label="University Name:"
|
||||
data_map=controller.data_map
|
||||
data_member="college_name"
|
||||
placeholder="University of Example"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="University Street Address:"
|
||||
data_map=controller.data_map
|
||||
data_member="college_city"
|
||||
placeholder="123 Example Rd"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
// TODO Make this a select dropdown.
|
||||
<OutlinedTextField
|
||||
label="University State:"
|
||||
data_map=controller.data_map
|
||||
data_member="college_state"
|
||||
placeholder="The State of Panic"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="University ZIP:"
|
||||
data_map=controller.data_map
|
||||
data_member="college_zip"
|
||||
placeholder="12345"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
<Select
|
||||
label="Have you been sent an acceptance to the university?"
|
||||
data_map=controller.data_map
|
||||
data_member="college_acceptance"
|
||||
required=true
|
||||
value_list=vec!["Yes".to_string(), "No".to_string()]
|
||||
/>
|
||||
// TODO Make this a select dropdown.
|
||||
<OutlinedTextField
|
||||
label="Chosen Major:"
|
||||
data_map=controller.data_map
|
||||
data_member="major"
|
||||
placeholder="Computer Science"
|
||||
input_type=TextFieldType::Text
|
||||
required=true
|
||||
/>
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
<div class="flex flex-1" />
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::{Loading, MultiEntry, ValidatedForm};
|
||||
use crate::input;
|
||||
use crate::pages::student::form_setup::use_student_form;
|
||||
|
||||
#[component]
|
||||
pub fn StudentWorkExperiencePage() -> impl IntoView {
|
||||
let controller = use_student_form("workexp");
|
||||
|
||||
view! {
|
||||
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||
<div class="flex flex-1" />
|
||||
<div class="flex flex-col flex-2 mt-6">
|
||||
<ValidatedForm title="Student Work Experience" on_submit=controller.submit_action>
|
||||
<MultiEntry
|
||||
label="Work Experience:"
|
||||
description="Leave blank if not applicable."
|
||||
data_map=controller.data_map
|
||||
data_member="extracurricular"
|
||||
schema=vec![
|
||||
input!(
|
||||
Text, "job_title", "Job Title:", true, "Waiter, Cashier, etc..."
|
||||
),
|
||||
input!(Text, "employer", "Employer:", true, "Your employer's name..."),
|
||||
input!(Number, "start_date", "Start date:", true, "40"),
|
||||
input!(Number, "end_date", "End date:", true, "3"),
|
||||
input!(Number, "num_hours", "Number of hours completed:", true, "120"),
|
||||
]
|
||||
/>
|
||||
</ValidatedForm>
|
||||
</div>
|
||||
<div class="flex flex-1" />
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use crate::components::DashboardButton;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn TestPage() -> impl IntoView {
|
||||
view! {
|
||||
<div class="p-6 space-y-4 max-w-1/3">
|
||||
<DashboardButton
|
||||
title="Home Page"
|
||||
description="Navigate to Home Page"
|
||||
icon="Book.png"
|
||||
path="/"
|
||||
/>
|
||||
<DashboardButton
|
||||
title="Home Page"
|
||||
description="Navigate to Home Page"
|
||||
icon="Calendar.png"
|
||||
path="/"
|
||||
/>
|
||||
<DashboardButton
|
||||
title="Home Page"
|
||||
description="Navigate to Home Page"
|
||||
icon="Edit.png"
|
||||
path="/"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,43 @@ use crate::components::ActionButton;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::AuthSignal;
|
||||
use leptos_router::hooks::{use_navigate, use_location};
|
||||
|
||||
#[component]
|
||||
pub fn UnauthenticatedPage() -> impl IntoView {
|
||||
let auth = use_context::<AuthSignal>().expect("AuthSignal not present in LoginLink");
|
||||
let login_url = move || {
|
||||
let login_url = Memo::new(move |_| {
|
||||
auth.with(|auth| {
|
||||
auth.unauthenticated()
|
||||
.map(|unauthenticated| unauthenticated.login_url())
|
||||
})
|
||||
});
|
||||
|
||||
Effect::new(move || log!("Generated login url: {:?}", login_url.get()));
|
||||
|
||||
let navigate = use_navigate();
|
||||
let location = use_location();
|
||||
|
||||
let on_click = move |_| {
|
||||
let current_path = location.pathname.get();
|
||||
|
||||
let storage_opt = window().session_storage().ok().flatten();
|
||||
if let Some(session_storage) = storage_opt {
|
||||
let _ = session_storage.set_item("post_login_path", ¤t_path);
|
||||
}
|
||||
if let Some(url) = login_url.get() {
|
||||
navigate(&url, Default::default())
|
||||
}
|
||||
};
|
||||
|
||||
Effect::new(move || log!("Generated login url: {:?}", login_url()));
|
||||
|
||||
let navigate = leptos_router::hooks::use_navigate();
|
||||
|
||||
view! (
|
||||
<div class="flex h-screen justify-center items-center pb-50">
|
||||
view! {
|
||||
<div class="flex mx-auto h-screen justify-center items-center pb-50">
|
||||
<div class="bg-white p-8 border rounded-lg shadow-lg">
|
||||
<h3> "You're not signed in. Please sign in with your Region 15 email." </h3>
|
||||
<h3>"You're not signed in. Please sign in first."</h3>
|
||||
<div class="flex pt-4 justify-center items-center">
|
||||
<ActionButton on:click=move |_| {
|
||||
if let Some(url) = login_url() {
|
||||
navigate(&url, Default::default())
|
||||
}
|
||||
}>
|
||||
"Sign in"
|
||||
</ActionButton>
|
||||
<ActionButton on:click=on_click>"Sign in"</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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
|
||||
/// 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 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
/// access to an AuthSignal, or it will result in a total failure.
|
||||
pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> {
|
||||
let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal.");
|
||||
Signal::derive(move || {
|
||||
auth.with(|auth| {
|
||||
auth.authenticated().and_then(|data| {
|
||||
data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{Algorithm, AuthSignal, TokenData};
|
||||
use crate::common::UserClaims;
|
||||
|
||||
pub fn use_origin() -> String {
|
||||
#[cfg(target_arch = "wasm32")] {
|
||||
use leptos::web_sys;
|
||||
// Just get the current URL origin.
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.location()
|
||||
.origin()
|
||||
.unwrap_or("http://localhost:3000/".to_string())
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Option<TokenData<UserClaims>>> {
|
||||
let auth = expect_context::<AuthSignal>();
|
||||
Signal::derive(move || {
|
||||
auth.with(|auth| {
|
||||
auth.authenticated().and_then(|data| {
|
||||
data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"])
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
pub mod server;
|
||||
|
||||
mod common;
|
||||
pub use common::*;
|
||||
@@ -0,0 +1,17 @@
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
Reference in new issue
Block a user