From 5e45cc89fc5fe7c05bb2238b2d3c1c7365da933d Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Wed, 21 Jan 2026 12:49:49 -0500 Subject: [PATCH 01/56] feat: add new comparisons --- src/pages/api/comparisons.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/pages/api/comparisons.rs b/src/pages/api/comparisons.rs index 1022b1c..ceaf746 100644 --- a/src/pages/api/comparisons.rs +++ b/src/pages/api/comparisons.rs @@ -188,6 +188,17 @@ fn create_sports_comparisons() -> Vec { "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", + ), ] } @@ -356,6 +367,24 @@ fn make_comp_list() -> Vec { "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 mut sports_comps = create_sports_comparisons(); let mut major_comps = create_major_comparisons(); @@ -370,6 +399,8 @@ fn make_comp_list() -> Vec { attended_bas, midd_south, family_military, + youth_baseball, + aquatic_club ]; comp_list.append(&mut sports_comps); From 9e418dddf519401e045ebbba1b805026a0e72c5e Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Wed, 21 Jan 2026 13:55:53 -0500 Subject: [PATCH 02/56] feat: update existing comparisons --- src/app.rs | 3 ++- src/pages/api/comparisons.rs | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index 15af1a2..f502687 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,4 @@ -use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback}; +use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, TestPage}; use leptos::prelude::*; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_oidc::AuthSignal; @@ -61,6 +61,7 @@ pub fn AppWithRoutes() -> impl IntoView { // TODO Create a 404 page + // diff --git a/src/pages/api/comparisons.rs b/src/pages/api/comparisons.rs index ceaf746..679d6fa 100644 --- a/src/pages/api/comparisons.rs +++ b/src/pages/api/comparisons.rs @@ -263,12 +263,12 @@ fn create_major_comparisons() -> Vec { #[allow(unused)] fn make_comp_list() -> Vec { 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( From 4a2a5ca9ff2fa6d4da027dcafbcb28cc5720d5a7 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 22 Jan 2026 01:34:58 -0500 Subject: [PATCH 03/56] feat: create sidebar tabs list --- src/app.rs | 15 ++++++++- src/common/mod.rs | 3 +- src/common/tab_info.rs | 5 +++ src/components/banner.rs | 4 +-- src/components/login/admin.rs | 18 +++++++++++ src/components/login/mod.rs | 4 ++- src/components/mod.rs | 3 +- src/components/tabs/mod.rs | 2 ++ src/components/tabs/sidebar.rs | 57 ++++++++++++++++++++++++++++++++++ src/pages/admin/mod.rs | 2 ++ src/pages/admin/shell.rs | 49 +++++++++++++++++++++++++++++ src/pages/mod.rs | 3 +- 12 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 src/common/tab_info.rs create mode 100644 src/components/login/admin.rs create mode 100644 src/components/tabs/mod.rs create mode 100644 src/components/tabs/sidebar.rs create mode 100644 src/pages/admin/mod.rs create mode 100644 src/pages/admin/shell.rs diff --git a/src/app.rs b/src/app.rs index f502687..ec4ff85 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,4 @@ -use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, TestPage}; +use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, AdminShell, AdminTesting, AdminFallback}; use leptos::prelude::*; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_oidc::AuthSignal; @@ -64,6 +64,7 @@ pub fn AppWithRoutes() -> impl IntoView { // + @@ -101,6 +102,18 @@ fn LoanerRoutes() -> impl MatchNestedRoutes + Clone { .into_any_nested_route() } +#[component(transparent)] +fn AdminRoutes() -> impl MatchNestedRoutes + Clone { + view! { + + + + + } + .into_inner() + .into_any_nested_route() +} + #[component] pub fn AuthErrorView() -> impl IntoView { let auth = use_context::().expect("AuthError: RwSignal was not found"); diff --git a/src/common/mod.rs b/src/common/mod.rs index 487f959..cd627ea 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -7,8 +7,9 @@ mod submit_status; mod user; mod value_type; pub mod animations; +mod tab_info; pub use self::{ comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*, - submit_status::*, user::*, value_type::*, + submit_status::*, user::*, value_type::*, tab_info::*, }; diff --git a/src/common/tab_info.rs b/src/common/tab_info.rs new file mode 100644 index 0000000..f292629 --- /dev/null +++ b/src/common/tab_info.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone)] +pub struct TabInfo { + pub name: String, + pub path: String, +} \ No newline at end of file diff --git a/src/components/banner.rs b/src/components/banner.rs index 8a26ec3..d82b994 100644 --- a/src/components/banner.rs +++ b/src/components/banner.rs @@ -22,7 +22,7 @@ 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, /// The route that the navigates user to a page (home page). #[prop(into, optional)] path: Signal, @@ -44,7 +44,7 @@ pub fn Banner(
icon 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()} + } +} \ No newline at end of file diff --git a/src/components/login/mod.rs b/src/components/login/mod.rs index fae50eb..c9911f2 100644 --- a/src/components/login/mod.rs +++ b/src/components/login/mod.rs @@ -1,5 +1,7 @@ +pub(crate) mod setup; mod provider; pub use provider::*; mod student; pub use student::*; -pub(crate) mod setup; \ No newline at end of file +mod admin; +pub use admin::*; \ No newline at end of file diff --git a/src/components/mod.rs b/src/components/mod.rs index 27d6b69..89dc30c 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -13,9 +13,10 @@ 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::*, date::* + validated_form::*, toasts::*, header::*, date::*, tabs::* }; diff --git a/src/components/tabs/mod.rs b/src/components/tabs/mod.rs new file mode 100644 index 0000000..2c727dc --- /dev/null +++ b/src/components/tabs/mod.rs @@ -0,0 +1,2 @@ +mod sidebar; +pub use sidebar::*; \ No newline at end of file diff --git a/src/components/tabs/sidebar.rs b/src/components/tabs/sidebar.rs new file mode 100644 index 0000000..166e5db --- /dev/null +++ b/src/components/tabs/sidebar.rs @@ -0,0 +1,57 @@ +use leptos::prelude::*; +use leptos_router::components::{Outlet, A}; +use leptos_router::hooks::use_location; +use crate::common::TabInfo; + +#[component] +fn SidebarTab( + #[prop(into)] name: Signal, + #[prop(into)] path: Signal +) -> impl IntoView { + let location = use_location(); + + 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(&path.get()) + }); + + view! { + +
+ {name} +
+
+ } +} + +#[component] +pub fn TabSidebarList( + #[prop(into)] tabs: Signal>, +) -> impl IntoView { + // Create a series of tabs based on a given list of names and paths. + + view! { +
+ + +
+ +
+
+ } +} diff --git a/src/pages/admin/mod.rs b/src/pages/admin/mod.rs new file mode 100644 index 0000000..d805db1 --- /dev/null +++ b/src/pages/admin/mod.rs @@ -0,0 +1,2 @@ +mod shell; +pub use shell::*; \ No newline at end of file diff --git a/src/pages/admin/shell.rs b/src/pages/admin/shell.rs new file mode 100644 index 0000000..786c3b1 --- /dev/null +++ b/src/pages/admin/shell.rs @@ -0,0 +1,49 @@ +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! { + + + // We'll put the tabs here. + + + } +} + +#[component] +pub fn AdminFallback() -> impl IntoView { + view! { +
+

"This is a fallback page"

+
+ } +} + +#[component] +pub fn AdminTesting() -> impl IntoView { + view! { +
+

"This is a testing page"

+
+ } +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 4cdf400..e1bf618 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -10,9 +10,10 @@ mod unauthenticated_page; mod provider_contact; mod auth_callback; mod provider_applicants; +mod admin; pub use self::{ about_page::*, comparison_test::*, home_page::*, loaner_page::*, provider_portal::*, test_page::*, unauthenticated_page::*, scholarship_info::*, provider_contact::*, - auth_callback::*, provider_applicants::* + auth_callback::*, provider_applicants::*, admin::* }; From 9427810026d0f49d52ad32d7c453ddb8f56ecffc Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 22 Jan 2026 11:18:13 -0500 Subject: [PATCH 04/56] style: rustfmt run --- src/components/login/admin.rs | 4 +--- src/components/tabs/sidebar.rs | 15 +++++++-------- src/pages/admin/shell.rs | 22 ++++++++++------------ src/pages/provider_applicants.rs | 4 +++- src/pages/scholarship_info.rs | 2 +- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/components/login/admin.rs b/src/components/login/admin.rs index 024562c..0510f31 100644 --- a/src/components/login/admin.rs +++ b/src/components/login/admin.rs @@ -12,7 +12,5 @@ pub fn AdminLoginContext( "/providers/callback" ); - view! { - {children()} - } + view! { {children()} } } \ No newline at end of file diff --git a/src/components/tabs/sidebar.rs b/src/components/tabs/sidebar.rs index 166e5db..1155626 100644 --- a/src/components/tabs/sidebar.rs +++ b/src/components/tabs/sidebar.rs @@ -18,10 +18,12 @@ fn SidebarTab( }); view! { - -
+
+ class=(["bg-white", "text-black"], move || selected.get()) + > {name}
@@ -40,12 +42,9 @@ pub fn TabSidebarList( - + diff --git a/src/pages/admin/shell.rs b/src/pages/admin/shell.rs index 786c3b1..93ad9c3 100644 --- a/src/pages/admin/shell.rs +++ b/src/pages/admin/shell.rs @@ -14,18 +14,16 @@ pub fn AdminShell() -> impl IntoView { // We'll put the tabs here. - + } } diff --git a/src/pages/provider_applicants.rs b/src/pages/provider_applicants.rs index 644750b..181e907 100644 --- a/src/pages/provider_applicants.rs +++ b/src/pages/provider_applicants.rs @@ -8,7 +8,9 @@ pub fn ApplicantsPageFallback() -> impl IntoView {

"Applicants Page"

-

"This page is under construction! It will show all the students that are eligible for your scholarship(s)."

+

+ "This page is under construction! It will show all the students that are eligible for your scholarship(s)." +

"Come back soon!"

} diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index 7d561a4..895a443 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -639,7 +639,7 @@ fn ScholarshipForm(
Date: Fri, 23 Jan 2026 13:56:13 -0500 Subject: [PATCH 05/56] feat: update unauthenticated page --- src/pages/unauthenticated_page.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/unauthenticated_page.rs b/src/pages/unauthenticated_page.rs index e2d6857..2b66d9d 100644 --- a/src/pages/unauthenticated_page.rs +++ b/src/pages/unauthenticated_page.rs @@ -34,7 +34,7 @@ pub fn UnauthenticatedPage() -> impl IntoView { view! {
-

"You're not signed in. Please sign in with your Region 15 email."

+

"You're not signed in. Please sign in first."

"Sign in"
From aa912673c08a3ad1c083d9369eaaace0c136e099 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Mon, 26 Jan 2026 16:42:32 -0500 Subject: [PATCH 06/56] feat: implement basic selectable nav list --- src/app.rs | 7 +- src/common/tab_info.rs | 11 +- src/components/lists/multi/mod.rs | 4 +- src/components/lists/multi/select_nav_list.rs | 154 ++++++++++++++++++ src/components/tabs/sidebar.rs | 53 +++++- src/pages/admin/home_page.rs | 11 ++ src/pages/admin/mod.rs | 8 +- src/pages/admin/provider_page.rs | 30 ++++ src/pages/admin/scholarship_page.rs | 15 ++ src/pages/admin/shell.rs | 11 +- 10 files changed, 285 insertions(+), 19 deletions(-) create mode 100644 src/components/lists/multi/select_nav_list.rs create mode 100644 src/pages/admin/home_page.rs create mode 100644 src/pages/admin/provider_page.rs create mode 100644 src/pages/admin/scholarship_page.rs diff --git a/src/app.rs b/src/app.rs index ec4ff85..87f4682 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,4 @@ -use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, AdminShell, AdminTesting, AdminFallback}; +use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, AdminShell, AdminHomePage, AdminProviderPage, AdminScholarshipPage}; use leptos::prelude::*; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_oidc::AuthSignal; @@ -106,8 +106,9 @@ fn LoanerRoutes() -> impl MatchNestedRoutes + Clone { fn AdminRoutes() -> impl MatchNestedRoutes + Clone { view! { - - + + + } .into_inner() diff --git a/src/common/tab_info.rs b/src/common/tab_info.rs index f292629..f2d0872 100644 --- a/src/common/tab_info.rs +++ b/src/common/tab_info.rs @@ -1,5 +1,14 @@ #[derive(Debug, Clone)] pub struct TabInfo { - pub name: String, + pub text: String, pub path: String, +} + +impl TabInfo { + pub fn new(text: impl Into, path: impl Into) -> Self { + Self { + text: text.into(), + path: path.into(), + } + } } \ No newline at end of file diff --git a/src/components/lists/multi/mod.rs b/src/components/lists/multi/mod.rs index c3fcbbc..717cb89 100644 --- a/src/components/lists/multi/mod.rs +++ b/src/components/lists/multi/mod.rs @@ -1,6 +1,8 @@ mod utils; mod checkbox; mod chips; +mod select_nav_list; pub use checkbox::*; -pub use chips::*; \ No newline at end of file +pub use chips::*; +pub use select_nav_list::*; \ No newline at end of file diff --git a/src/components/lists/multi/select_nav_list.rs b/src/components/lists/multi/select_nav_list.rs new file mode 100644 index 0000000..a6afdf9 --- /dev/null +++ b/src/components/lists/multi/select_nav_list.rs @@ -0,0 +1,154 @@ +use leptos::logging::log; +use leptos::prelude::*; +use leptos_router::components::A; +use crate::components::utils; + +#[derive(Debug, Clone)] +pub struct NavListInfo { + pub text: String, + pub id: String, + pub slug: String, +} + +#[component] +fn NavListItem( + #[prop(into)] name: Signal, + #[prop(into)] value: Signal, + #[prop(into)] text: Signal, + #[prop(into)] slug: Signal, + #[prop(into)] selected: Signal, + #[prop(into)] on_change: Callback<()>, +) -> impl IntoView { + view! { + + } +} + +#[component] +fn NavListCheckbox( + #[prop(into)] selected: Signal, + #[prop(into)] on_change: Callback<()>, + #[prop(into)] name: Signal, + #[prop(into)] value: Signal, +) -> impl IntoView { + let id = Memo::new(move |_| utils::create_unique_id(&name.get(), &value.get())); + + view! { + + } +} + +/// # Selectable Nav-List Component +/// +/// This component takes a `Vec` 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>, + #[prop(into)] items: Signal>, + #[prop(into)] name: Signal, +) -> 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::>() + ); + } + }; + + view! { +
+ // 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. +
+ +
+ // Items block - contains constructed items. These are selectable. + + } + } + /> +
+ } +} diff --git a/src/components/tabs/sidebar.rs b/src/components/tabs/sidebar.rs index 1155626..bc9ee53 100644 --- a/src/components/tabs/sidebar.rs +++ b/src/components/tabs/sidebar.rs @@ -5,7 +5,7 @@ use crate::common::TabInfo; #[component] fn SidebarTab( - #[prop(into)] name: Signal, + #[prop(into)] text: Signal, #[prop(into)] path: Signal ) -> impl IntoView { let location = use_location(); @@ -24,12 +24,55 @@ fn SidebarTab( class=(["bg-transparent", "text-white"], move || !selected.get()) class=(["bg-white", "text-black"], move || selected.get()) > - {name} + {text}
} } +/// # Tab Sidebar Component +/// +/// This component creates a list of tabs that correspond to the given `Vec`. +/// 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! { +/// +/// // Remember, no Outlets! +/// } +/// ``` #[component] pub fn TabSidebarList( #[prop(into)] tabs: Signal>, @@ -41,10 +84,10 @@ pub fn TabSidebarList( diff --git a/src/pages/admin/home_page.rs b/src/pages/admin/home_page.rs new file mode 100644 index 0000000..b0f0f45 --- /dev/null +++ b/src/pages/admin/home_page.rs @@ -0,0 +1,11 @@ +use leptos::prelude::*; + +#[component] +pub fn AdminHomePage() -> impl IntoView { + view! { +
+
"Admin Home Page"
+
"This is just a placeholder for now."
+
+ } +} diff --git a/src/pages/admin/mod.rs b/src/pages/admin/mod.rs index d805db1..d5400ea 100644 --- a/src/pages/admin/mod.rs +++ b/src/pages/admin/mod.rs @@ -1,2 +1,8 @@ mod shell; -pub use shell::*; \ No newline at end of file +pub use shell::*; +mod home_page; +pub use home_page::*; +mod provider_page; +pub use provider_page::*; +mod scholarship_page; +pub use scholarship_page::*; diff --git a/src/pages/admin/provider_page.rs b/src/pages/admin/provider_page.rs new file mode 100644 index 0000000..98ab694 --- /dev/null +++ b/src/pages/admin/provider_page.rs @@ -0,0 +1,30 @@ +use leptos::prelude::*; +use crate::components::{NavListInfo, SelectableNavList}; + +#[component] +pub fn AdminProviderPage() -> impl IntoView { + let selected_items = RwSignal::new(Vec::new()); + let provider_list = RwSignal::new(vec![ + NavListInfo { + text: "Testing 1".to_string(), + id: "1".to_string(), + slug: "1".to_string(), + }, + NavListInfo { + text: "Testing 2".to_string(), + id: "2".to_string(), + slug: "2".to_string(), + } + ]); + + view! { +
+
"Provider View"
+ +
+ } +} diff --git a/src/pages/admin/scholarship_page.rs b/src/pages/admin/scholarship_page.rs new file mode 100644 index 0000000..c3b13ab --- /dev/null +++ b/src/pages/admin/scholarship_page.rs @@ -0,0 +1,15 @@ +use leptos::prelude::*; + +#[component] +pub fn AdminScholarshipPage() -> impl IntoView { + view! { +
+
+ "Scholarships View" +
+
+ "This is a placeholder for now. It will show a list of all scholarships." +
+
+ } +} \ No newline at end of file diff --git a/src/pages/admin/shell.rs b/src/pages/admin/shell.rs index 93ad9c3..56aacbb 100644 --- a/src/pages/admin/shell.rs +++ b/src/pages/admin/shell.rs @@ -15,14 +15,9 @@ pub fn AdminShell() -> impl IntoView { // We'll put the tabs here. } From 12a980f327250c99d0e5c1a06c8a9f70b811962c Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 27 Jan 2026 00:48:17 -0500 Subject: [PATCH 07/56] feat: add headers to nav list --- src/components/lists/multi/select_nav_list.rs | 39 ++++++++++++++++--- src/pages/admin/provider_page.rs | 14 ++++++- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/components/lists/multi/select_nav_list.rs b/src/components/lists/multi/select_nav_list.rs index a6afdf9..9c320cd 100644 --- a/src/components/lists/multi/select_nav_list.rs +++ b/src/components/lists/multi/select_nav_list.rs @@ -5,7 +5,7 @@ use crate::components::utils; #[derive(Debug, Clone)] pub struct NavListInfo { - pub text: String, + pub text: Vec, pub id: String, pub slug: String, } @@ -14,11 +14,25 @@ pub struct NavListInfo { fn NavListItem( #[prop(into)] name: Signal, #[prop(into)] value: Signal, - #[prop(into)] text: Signal, + #[prop(into)] text: Signal>, #[prop(into)] slug: Signal, #[prop(into)] selected: Signal, #[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::>() + ); + view! { } } @@ -80,6 +101,7 @@ fn NavListCheckbox( pub fn SelectableNavList( #[prop(into)] selected: RwSignal>, #[prop(into)] items: Signal>, + #[prop(into)] headers: Signal>, #[prop(into)] name: Signal, ) -> impl IntoView { let all_selected = RwSignal::new(false); @@ -106,13 +128,20 @@ pub fn SelectableNavList( // 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. -
+
+ +
{header_text}
+
// Items block - contains constructed items. These are selectable. impl IntoView { let selected_items = RwSignal::new(Vec::new()); let provider_list = RwSignal::new(vec![ NavListInfo { - text: "Testing 1".to_string(), + text: vec!["Testing 1".to_string(), 3.to_string(), 500.to_string()], id: "1".to_string(), slug: "1".to_string(), }, NavListInfo { - text: "Testing 2".to_string(), + text: vec!["Testing 2".to_string(), 2.to_string(), 1000.to_string()], id: "2".to_string(), slug: "2".to_string(), } ]); + + let headers = vec![ + "Scholarship Name", + "Number of Awards", + "Amount of Awards" + ] + .iter() + .map(|s| s.to_string()) + .collect::>(); view! {
@@ -24,6 +33,7 @@ pub fn AdminProviderPage() -> impl IntoView { selected=selected_items name="providers-nav-list" items=provider_list + headers=headers />
} From a95e9a1034eabfc94d0ead124cf18e4f59a2811b Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 27 Jan 2026 14:13:55 -0500 Subject: [PATCH 08/56] feat: create full provider list setup --- src/pages/admin/provider_page.rs | 71 ++++++++++++++++++++++++------- src/pages/api/admin.rs | 44 +++++++++++++++++++ src/pages/api/comparisons.rs | 5 ++- src/pages/api/dates.rs | 2 +- src/pages/api/mod.rs | 22 ++++++++-- src/pages/api/providers.rs | 70 +++++++++++++++++++++++++++++++ src/pages/api/scholarships.rs | 2 +- src/pages/provider_contact.rs | 72 ++------------------------------ 8 files changed, 198 insertions(+), 90 deletions(-) create mode 100644 src/pages/api/admin.rs create mode 100644 src/pages/api/providers.rs diff --git a/src/pages/admin/provider_page.rs b/src/pages/admin/provider_page.rs index b7b88ff..92ac896 100644 --- a/src/pages/admin/provider_page.rs +++ b/src/pages/admin/provider_page.rs @@ -1,26 +1,69 @@ +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![ - NavListInfo { - text: vec!["Testing 1".to_string(), 3.to_string(), 500.to_string()], - id: "1".to_string(), - slug: "1".to_string(), - }, - NavListInfo { - text: vec!["Testing 2".to_string(), 2.to_string(), 1000.to_string()], - id: "2".to_string(), - slug: "2".to_string(), + 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![ - "Scholarship Name", - "Number of Awards", - "Amount of Awards" + "Email", + "First Name", + "Last Name" ] .iter() .map(|s| s.to_string()) diff --git a/src/pages/api/admin.rs b/src/pages/api/admin.rs new file mode 100644 index 0000000..288c7bc --- /dev/null +++ b/src/pages/api/admin.rs @@ -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>, 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::>>() + } else { + Vec::new() + } + }) + .map_err(ServerFnError::from) +} diff --git a/src/pages/api/comparisons.rs b/src/pages/api/comparisons.rs index 679d6fa..49c6e02 100644 --- a/src/pages/api/comparisons.rs +++ b/src/pages/api/comparisons.rs @@ -7,6 +7,9 @@ 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, @@ -15,8 +18,6 @@ use std::collections::HashMap; use leptos::prelude::ServerFnError; use leptos::server; -#[cfg(feature = "ssr")] -static COMPARISONS_TABLE: &str = "leptos-comparisons"; /// 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. diff --git a/src/pages/api/dates.rs b/src/pages/api/dates.rs index ee3a6a7..3cbf387 100644 --- a/src/pages/api/dates.rs +++ b/src/pages/api/dates.rs @@ -4,7 +4,7 @@ use leptos::prelude::*; use crate::common::DateInfo; #[cfg(feature = "ssr")] -static DATES_TABLE: &str = "leptos-dates"; +use super::DATES_TABLE; #[cfg(feature = "ssr")] use aws_sdk_dynamodb::{ diff --git a/src/pages/api/mod.rs b/src/pages/api/mod.rs index f7aa9bf..a34655e 100644 --- a/src/pages/api/mod.rs +++ b/src/pages/api/mod.rs @@ -1,7 +1,23 @@ mod comparisons; -pub use comparisons::*; mod scholarships; -pub use scholarships::*; mod dates; -pub use dates::*; +mod admin; +mod providers; +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"; diff --git a/src/pages/api/providers.rs b/src/pages/api/providers.rs new file mode 100644 index 0000000..76964bc --- /dev/null +++ b/src/pages/api/providers.rs @@ -0,0 +1,70 @@ +use leptos::prelude::*; +use leptos::logging::log; +use std::collections::HashMap; +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, 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(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::>(); + + Ok(map) + } + Err(err) => { + let msg = err.message().unwrap_or("An unknown error occurred."); + Err(ServerFnError::new(msg)) + } + } +} + +#[server] +pub async fn put_provider_contact(id: String, contact_info: HashMap) -> 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)) + } + } +} \ No newline at end of file diff --git a/src/pages/api/scholarships.rs b/src/pages/api/scholarships.rs index 5101af6..0e5dd5a 100644 --- a/src/pages/api/scholarships.rs +++ b/src/pages/api/scholarships.rs @@ -18,7 +18,7 @@ use leptos::prelude::ServerFnError; use leptos::server; #[cfg(feature = "ssr")] -static SCHOLARSHIPS_TABLE: &str = "leptos-scholarships"; +use super::SCHOLARSHIPS_TABLE; #[server(GetScholarshipInfo)] pub async fn get_scholarship_info(id: String) -> Result { diff --git a/src/pages/provider_contact.rs b/src/pages/provider_contact.rs index f1c9ddc..b560cf6 100644 --- a/src/pages/provider_contact.rs +++ b/src/pages/provider_contact.rs @@ -1,77 +1,11 @@ -#[cfg(feature = "ssr")] -use crate::utils::server::{create_dynamo_client, into_attr_map}; - -use std::collections::HashMap; +use std::collections::HashMap; use leptos::logging::log; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; -use crate::common::ValueType; -use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm}; +use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ValidatedForm}; use crate::pages::UnauthenticatedPage; use crate::utils::get_user_claims; - -#[cfg(feature = "ssr")] -static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts"; - -#[server] -async fn get_provider_contact(id: String) -> Result, 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(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::>(); - - 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) -> 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)) - } - } -} +use super::api::{get_provider_contact, PutProviderContact}; #[component] pub fn ProviderContactPage() -> impl IntoView { From 3b9e2598b4de390455ba5f264550a49ec03261d3 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Wed, 28 Jan 2026 11:31:06 -0500 Subject: [PATCH 09/56] style: rustfmt run --- src/components/lists/multi/select_nav_list.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/lists/multi/select_nav_list.rs b/src/components/lists/multi/select_nav_list.rs index 9c320cd..3ce9677 100644 --- a/src/components/lists/multi/select_nav_list.rs +++ b/src/components/lists/multi/select_nav_list.rs @@ -44,14 +44,14 @@ fn NavListItem( on_change=move || on_change.run(()) selected=selected /> - + {linked_info} - +

{info}

@@ -135,11 +135,7 @@ pub fn SelectableNavList( value="select-all" on_change=all_selected_change /> - +
{header_text}
From 765322bccac676980eae648bf12481d2abcc4984 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Wed, 28 Jan 2026 14:36:48 -0500 Subject: [PATCH 10/56] feat: begin implementation of student forms --- src/app.rs | 15 ++++- src/components/login/student.rs | 7 ++- src/components/tabs/sidebar.rs | 2 +- src/pages/mod.rs | 3 +- src/pages/student/demographics_page.rs | 79 ++++++++++++++++++++++++++ src/pages/student/home_page.rs | 11 ++++ src/pages/student/mod.rs | 7 +++ src/pages/student/shell.rs | 20 +++++++ 8 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 src/pages/student/demographics_page.rs create mode 100644 src/pages/student/home_page.rs create mode 100644 src/pages/student/mod.rs create mode 100644 src/pages/student/shell.rs diff --git a/src/app.rs b/src/app.rs index 87f4682..e33567e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,4 @@ -use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, AdminShell, AdminHomePage, AdminProviderPage, AdminScholarshipPage}; +use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, AdminShell, AdminHomePage, AdminProviderPage, AdminScholarshipPage, StudentShell, StudentHomePage, StudentDemographicsPage}; use leptos::prelude::*; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_oidc::AuthSignal; @@ -65,6 +65,7 @@ pub fn AppWithRoutes() -> impl IntoView { + @@ -115,6 +116,18 @@ fn AdminRoutes() -> impl MatchNestedRoutes + Clone { .into_any_nested_route() } +#[component(transparent)] +fn StudentRoutes() -> impl MatchNestedRoutes + Clone { + view! { + + + + + } + .into_inner() + .into_any_nested_route() +} + #[component] pub fn AuthErrorView() -> impl IntoView { let auth = use_context::().expect("AuthError: RwSignal was not found"); diff --git a/src/components/login/student.rs b/src/components/login/student.rs index 14becf8..d5c7c54 100644 --- a/src/components/login/student.rs +++ b/src/components/login/student.rs @@ -1,5 +1,4 @@ use leptos::prelude::*; -use leptos_router::components::Outlet; use super::setup::provide_auth_context; ///# Student Login Context Component @@ -16,7 +15,9 @@ use super::setup::provide_auth_context; /// /// } #[component] -pub fn StudentLoginContext() -> impl IntoView { +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", @@ -24,5 +25,5 @@ pub fn StudentLoginContext() -> impl IntoView { "/students/callback" ); - view! { } + view! { {children()} } } diff --git a/src/components/tabs/sidebar.rs b/src/components/tabs/sidebar.rs index bc9ee53..5a577df 100644 --- a/src/components/tabs/sidebar.rs +++ b/src/components/tabs/sidebar.rs @@ -91,7 +91,7 @@ pub fn TabSidebarList( -
+
diff --git a/src/pages/mod.rs b/src/pages/mod.rs index e1bf618..22595de 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -11,9 +11,10 @@ mod provider_contact; mod auth_callback; mod provider_applicants; mod admin; +mod student; pub use self::{ about_page::*, comparison_test::*, home_page::*, loaner_page::*, provider_portal::*, test_page::*, unauthenticated_page::*, scholarship_info::*, provider_contact::*, - auth_callback::*, provider_applicants::*, admin::* + auth_callback::*, provider_applicants::*, admin::*, student::*, }; diff --git a/src/pages/student/demographics_page.rs b/src/pages/student/demographics_page.rs new file mode 100644 index 0000000..c06e91c --- /dev/null +++ b/src/pages/student/demographics_page.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; +use leptos::prelude::*; +use crate::components::{OutlinedTextField, Select, TextFieldType, ValidatedForm}; + +#[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 data_map = RwSignal::new(HashMap::new()); + + view! { +
+
+ + // Inputs here. + + + // There must be a date of birth selection field here. + + + + + - impl IntoView {
impl IntoView { required=true /> impl IntoView {
Date: Mon, 2 Feb 2026 14:34:32 -0500 Subject: [PATCH 24/56] feat: add fallback text --- src/components/multi_entry.rs | 90 +++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/src/components/multi_entry.rs b/src/components/multi_entry.rs index 6a6ce80..8f7449b 100644 --- a/src/components/multi_entry.rs +++ b/src/components/multi_entry.rs @@ -75,8 +75,8 @@ pub fn MultiEntry( #[prop()] data_map: RwSignal>, /// The type and order of the inputs for each `Entry` component within this `MultiEntry` component. - #[prop(optional)] - schema: Vec, + #[prop(optional, into)] + schema: Signal>, ) -> 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. @@ -85,9 +85,8 @@ pub fn MultiEntry( .get_untracked() .get(&data_member) .unwrap_or(&ValueType::List(None)) - .as_list() - .unwrap_or(Some(Vec::::new())) - .unwrap_or(Vec::new()), + .as_list().ok().flatten() + .unwrap_or(Vec::new()) ); // This effect updates the data_map with any new values from the data_reactive list, every time @@ -103,42 +102,51 @@ pub fn MultiEntry( let add_entry = move |_| data_reactive.update(|list| list.push(new_entry())); view! { -
- {label} - - {description} - +
+
+ {label} + + {description} + +
// Render the entries - - } + "You haven't added any entries yet."
} - /> + > + + } + } + /> +
"Add entry" @@ -181,12 +189,12 @@ fn Entry( data_map: RwSignal>, /// The schema of this entry. Should contain all information about what inputs to use and what /// data members those inputs modify. - #[prop()] - schema: Vec, + #[prop(into)] + schema: Signal>, ) -> impl IntoView { view! {
- {schema.iter() + {schema.get().iter() .map(|input_type| { input_type.clone().into_view(data_map.clone()) }).collect_view() From 0ca00664b0810a8d880fcc3cc57e0966e8c1aa1b Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Mon, 2 Feb 2026 16:51:13 -0500 Subject: [PATCH 25/56] style: rustfmt run --- src/app.rs | 12 ++++-- src/components/multi_entry.rs | 47 ++++++++++------------ src/components/tabs/sidebar.rs | 17 ++++---- src/pages/admin/shell.rs | 2 +- src/pages/admin/utils_page.rs | 4 +- src/pages/student/athletics_page.rs | 16 +++++--- src/pages/student/extracurriculars_page.rs | 4 +- src/pages/student/shell.rs | 23 ++++++----- 8 files changed, 71 insertions(+), 54 deletions(-) diff --git a/src/app.rs b/src/app.rs index 200270d..0ecab58 100644 --- a/src/app.rs +++ b/src/app.rs @@ -125,11 +125,17 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone { - + - - + + } .into_inner() diff --git a/src/components/multi_entry.rs b/src/components/multi_entry.rs index 8f7449b..41ad518 100644 --- a/src/components/multi_entry.rs +++ b/src/components/multi_entry.rs @@ -113,43 +113,40 @@ pub fn MultiEntry( // Render the entries "You haven't added any entries yet."
+ fallback=|| { + view! {
"You haven't added any entries yet."
} } > - } + view! { } } />
- "Add entry" + "Add entry"
} } @@ -194,11 +191,11 @@ fn Entry( ) -> impl IntoView { view! {
- {schema.get().iter() - .map(|input_type| { - input_type.clone().into_view(data_map.clone()) - }).collect_view() - } + {schema + .get() + .iter() + .map(|input_type| { input_type.clone().into_view(data_map.clone()) }) + .collect_view()}
} } diff --git a/src/components/tabs/sidebar.rs b/src/components/tabs/sidebar.rs index 72ca9ef..4ecaf33 100644 --- a/src/components/tabs/sidebar.rs +++ b/src/components/tabs/sidebar.rs @@ -1,5 +1,4 @@ -use leptos::logging::log; -use leptos::prelude::*; +use leptos::prelude::*; use leptos_router::components::{Outlet, A}; use leptos_router::hooks::use_location; use crate::common::{SubTabInfo, TabInfo}; @@ -94,17 +93,13 @@ fn SidebarTab( }); view! { -
-
- {text} -
+
{text}
+ {text} } diff --git a/src/pages/admin/shell.rs b/src/pages/admin/shell.rs index d52b219..77c5192 100644 --- a/src/pages/admin/shell.rs +++ b/src/pages/admin/shell.rs @@ -14,7 +14,7 @@ pub fn AdminShell() -> impl IntoView { // We'll put the tabs here. - impl IntoView { view! {
-
"This utility page provides buttons to initialize the corresponding lists of information."
+
+ "This utility page provides buttons to initialize the corresponding lists of information." +
"Create Comparisons" "Create Dates"
diff --git a/src/pages/student/athletics_page.rs b/src/pages/student/athletics_page.rs index 78e7ce3..7030ab7 100644 --- a/src/pages/student/athletics_page.rs +++ b/src/pages/student/athletics_page.rs @@ -17,7 +17,8 @@ pub fn StudentAthleticsPage() -> impl IntoView { data_map=data_map data_member="athletic_participation" schema=vec![ - input!(Select, "sport_name", "Sport Name:", true, [ + input!( + Select, "sport_name", "Sport Name:", true, [ "Football", "Soccer", "Cheerleading", @@ -34,14 +35,19 @@ pub fn StudentAthleticsPage() -> impl IntoView { "Softball", "Tennis", "Baseball" - ]), - input!(Checkbox, "grades", "Grades Participated:", true, [ + ] + ), + input!( + Checkbox, "grades", "Grades Participated:", true, [ "9th", "10th", "11th", "12th" - ]), - input!(Text, "achievements", "Special Achievements:", false, "Example 1, Example 2, etc...") + ] + ), + input!( + Text, "achievements", "Special Achievements:", false, "Example 1, Example 2, etc..." + ), ] /> diff --git a/src/pages/student/extracurriculars_page.rs b/src/pages/student/extracurriculars_page.rs index 1ec3a39..37f93e1 100644 --- a/src/pages/student/extracurriculars_page.rs +++ b/src/pages/student/extracurriculars_page.rs @@ -30,7 +30,9 @@ pub fn StudentExtracurricularsPage() -> impl IntoView { 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"]) + input!( + Checkbox, "grades", "Grades Participated:", true, ["9th", "10th", "11th", "12th"] + ), ] /> diff --git a/src/pages/student/shell.rs b/src/pages/student/shell.rs index 1422671..a85cbb9 100644 --- a/src/pages/student/shell.rs +++ b/src/pages/student/shell.rs @@ -18,15 +18,20 @@ pub fn StudentShell() -> impl IntoView { 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("Parent Info", "parent-info") - ])), + 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("Parent Info", "parent-info"), + ], + ), + ), ] /> From 06364236c5a2b5dd57a52e66b7da52ddf27d45f4 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 3 Feb 2026 10:05:44 -0500 Subject: [PATCH 26/56] feat: implement most of the university page, waiting for more information --- src/app.rs | 4 ++ src/pages/student/mod.rs | 4 +- src/pages/student/shell.rs | 2 +- src/pages/student/university_page.rs | 73 ++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/pages/student/university_page.rs diff --git a/src/app.rs b/src/app.rs index 0ecab58..a9d2f5e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -136,6 +136,10 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone { path=path!("additional/work-experience") view=student::StudentWorkExperiencePage /> + } .into_inner() diff --git a/src/pages/student/mod.rs b/src/pages/student/mod.rs index 4b739f8..657b444 100644 --- a/src/pages/student/mod.rs +++ b/src/pages/student/mod.rs @@ -6,6 +6,7 @@ mod athletics_page; mod academics_page; mod extracurriculars_page; mod work_experience_page; +mod university_page; pub use shell::*; pub use home_page::*; @@ -14,4 +15,5 @@ pub use additional_page::*; pub use athletics_page::*; pub use academics_page::*; pub use extracurriculars_page::*; -pub use work_experience_page::*; \ No newline at end of file +pub use work_experience_page::*; +pub use university_page::*; \ No newline at end of file diff --git a/src/pages/student/shell.rs b/src/pages/student/shell.rs index a85cbb9..3387a91 100644 --- a/src/pages/student/shell.rs +++ b/src/pages/student/shell.rs @@ -28,7 +28,7 @@ pub fn StudentShell() -> impl IntoView { SubTabInfo::new("Work Experience", "work-experience"), SubTabInfo::new("Extracurriculars", "extracurriculars"), SubTabInfo::new("University Info", "university"), - SubTabInfo::new("Parent Info", "parent-info"), + SubTabInfo::new("Family Info", "family-info"), ], ), ), diff --git a/src/pages/student/university_page.rs b/src/pages/student/university_page.rs new file mode 100644 index 0000000..5d58f1a --- /dev/null +++ b/src/pages/student/university_page.rs @@ -0,0 +1,73 @@ +use leptos::prelude::*; +use std::collections::HashMap; +use crate::components::{OutlinedTextField, Select, TextFieldType, ValidatedForm}; + +#[component] +pub fn StudentUniversityPage() -> impl IntoView { + let data_map = RwSignal::new(HashMap::new()); + + // college name, state, city, zip + // major, study field, intended career, college_acceptance + + view! { +
+
+ + // Inputs here. + + + // TODO Make this a select dropdown. + + +
diff --git a/src/components/multi_entry.rs b/src/components/multi_entry.rs index 4a2ffec..80d34fe 100644 --- a/src/components/multi_entry.rs +++ b/src/components/multi_entry.rs @@ -194,7 +194,7 @@ fn Entry( ) -> impl IntoView { view! {
- {schema + {move || schema .get() .iter() .map(|input_type| diff --git a/src/components/validated_form.rs b/src/components/validated_form.rs index 3e0d834..4420ccb 100644 --- a/src/components/validated_form.rs +++ b/src/components/validated_form.rs @@ -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] From 9b3f94f3ade9969e78a0e2991c679ffc13a8e9d6 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 5 Feb 2026 00:43:26 -0500 Subject: [PATCH 34/56] chore: remove unused code --- src/components/lists/single/select.rs | 36 --------------------------- 1 file changed, 36 deletions(-) diff --git a/src/components/lists/single/select.rs b/src/components/lists/single/select.rs index d77e67f..cd08b81 100644 --- a/src/components/lists/single/select.rs +++ b/src/components/lists/single/select.rs @@ -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; use crate::components::{use_validation_context, InputState, ValidationState}; fn validate(required: bool, value: String) -> ValidationState { @@ -36,16 +35,6 @@ pub fn Select( .unwrap_or_default() }) }); - - Effect::new(move || { - log!("Current select value: {}", value.get()); - }); - - // Effect::new(move || { - // data_map.update(|map| { - // map.insert(data_member.get(), ValueType::String(Some(raw_value.get()))); - // }); - // }); //#endregion //#region Validation Logic @@ -77,21 +66,6 @@ pub fn Select( map.insert(data_member.get(), ValueType::String(Some(value))); }); }; - - // let on_change = move |e: Targeted| { - // 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))); - // } - // }); - // }; //#endregion @@ -120,16 +94,6 @@ pub fn Select( key=|value| value.clone() children=move |val| view! { } /> - // {move || { - // value_list - // .get() - // .iter() - // .map(move |value| { - // let display = value.to_owned(); - // view! { } - // }) - // .collect::>() - // }}
From 38994088dfc833db02132509034397d48aefd9d6 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 5 Feb 2026 10:52:19 -0500 Subject: [PATCH 35/56] chore: recenter after recent changes --- src/pages/unauthenticated_page.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/unauthenticated_page.rs b/src/pages/unauthenticated_page.rs index 2b66d9d..f5317a3 100644 --- a/src/pages/unauthenticated_page.rs +++ b/src/pages/unauthenticated_page.rs @@ -32,7 +32,7 @@ pub fn UnauthenticatedPage() -> impl IntoView { }; view! { -
+

"You're not signed in. Please sign in first."

From bb743cf114331fbdc5bb6f828de807fd40d9010a Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 5 Feb 2026 11:00:10 -0500 Subject: [PATCH 36/56] chore: small QoL change --- src/utils/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/common.rs b/src/utils/common.rs index a80584d..6a6038d 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -22,7 +22,7 @@ pub fn use_origin() -> 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>> { - let auth = use_context::().expect("Couldn't find AuthSignal."); + let auth = expect_context::(); Signal::derive(move || { auth.with(|auth| { auth.authenticated().and_then(|data| { From 8417713aa1286789ca740d042640802d01e0a103 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 5 Feb 2026 12:03:48 -0500 Subject: [PATCH 37/56] feat: add delete button --- src/components/multi_entry.rs | 64 ++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/src/components/multi_entry.rs b/src/components/multi_entry.rs index 80d34fe..cb03016 100644 --- a/src/components/multi_entry.rs +++ b/src/components/multi_entry.rs @@ -3,6 +3,7 @@ use crate::components::ActionButton; use leptos::prelude::*; use std::collections::HashMap; use leptos::logging::log; +use leptos_icons::Icon; use uuid::Uuid; // region Helper Functions @@ -12,6 +13,13 @@ fn get_uuid_from_map(map: &HashMap) -> Option { map.get("uuid")?.as_string().unwrap_or(None) } +fn get_uuid(vt: ValueType) -> Option { + 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, data: &HashMap) { if let Some(uuid) = get_uuid_from_map(&data) { @@ -115,7 +123,7 @@ pub fn MultiEntry( {description}
-
+
// Render the entries } + + view! { + + } } /> @@ -191,18 +212,29 @@ fn Entry( #[prop(into)] schema: Signal>, /// The unique ID for this entry. #[prop(into)] id: Signal, + /// The function to run when the delete button is clicked. + #[prop(into)] on_delete: Callback<()>, ) -> impl IntoView { view! { -
- {move || schema - .get() - .iter() - .map(|input_type| - input_type - .clone() - .into_view(data_map.clone(), id.get()) - ) - .collect_view()} +
+ // Delete button +
+ +
+ + // Input section +
+ {move || { + schema + .get() + .iter() + .map(|input_type| input_type.clone().into_view(data_map.clone(), id.get())) + .collect_view() + }} +
} } From fe5a107c7c147c567983e0150998b96155d8de7b Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 5 Feb 2026 12:04:56 -0500 Subject: [PATCH 38/56] docs: add a comment to get_uuid --- src/components/multi_entry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/multi_entry.rs b/src/components/multi_entry.rs index cb03016..3140083 100644 --- a/src/components/multi_entry.rs +++ b/src/components/multi_entry.rs @@ -13,6 +13,7 @@ fn get_uuid_from_map(map: &HashMap) -> Option { 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 { vt.as_map().ok().flatten().unwrap_or_default() .get("uuid")? From fdc5374f177b9b669510122208553e9868891057 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 5 Feb 2026 12:11:09 -0500 Subject: [PATCH 39/56] chore: small styling changes --- src/components/loading.rs | 2 +- src/pages/student/academics_page.rs | 99 +++++++++++++++-------------- src/pages/student/athletics_page.rs | 95 ++++++++++++++------------- 3 files changed, 103 insertions(+), 93 deletions(-) diff --git a/src/components/loading.rs b/src/components/loading.rs index ee11ded..8c8a18c 100644 --- a/src/components/loading.rs +++ b/src/components/loading.rs @@ -3,7 +3,7 @@ use leptos::prelude::*; #[component] pub fn Loading() -> impl IntoView { view! { -
+
} diff --git a/src/pages/student/academics_page.rs b/src/pages/student/academics_page.rs index 559aa7b..f254903 100644 --- a/src/pages/student/academics_page.rs +++ b/src/pages/student/academics_page.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use crate::components::{OutlinedTextField, TextFieldType, ValidatedForm}; +use crate::components::{Loading, OutlinedTextField, TextFieldType, ValidatedForm}; use crate::pages::student::form_setup::use_student_form; #[component] @@ -7,51 +7,56 @@ pub fn StudentAcademicsPage() -> impl IntoView { let controller = use_student_form("academics"); view! { -
-
- - - - - - - -
-
+ +
+
+ + + + + + + +
+
+ } } \ No newline at end of file diff --git a/src/pages/student/athletics_page.rs b/src/pages/student/athletics_page.rs index 055f814..b311f1a 100644 --- a/src/pages/student/athletics_page.rs +++ b/src/pages/student/athletics_page.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use crate::components::{MultiEntry, ValidatedForm}; +use crate::components::{Loading, MultiEntry, ValidatedForm}; use crate::input; use crate::pages::student::form_setup::use_student_form; @@ -8,50 +8,55 @@ pub fn StudentAthleticsPage() -> impl IntoView { let controller = use_student_form("athletics"); view! { -
-
- - +
+
+ + - -
-
+ /> + +
+
+ } } \ No newline at end of file From 838a412376f1ab801e5db4b95a99ac2618f9720b Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 5 Feb 2026 12:15:46 -0500 Subject: [PATCH 40/56] feat: link student pages to database --- src/pages/student/demographics_page.rs | 22 +++++++++--------- src/pages/student/extracurriculars_page.rs | 12 +++++----- src/pages/student/family_info_page.rs | 26 +++++++++++----------- src/pages/student/university_page.rs | 18 +++++++-------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/pages/student/demographics_page.rs b/src/pages/student/demographics_page.rs index c06e91c..641f410 100644 --- a/src/pages/student/demographics_page.rs +++ b/src/pages/student/demographics_page.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; use leptos::prelude::*; use crate::components::{OutlinedTextField, Select, TextFieldType, ValidatedForm}; +use crate::pages::student::form_setup::use_student_form; #[component] pub fn StudentDemographicsPage() -> impl IntoView { @@ -8,17 +8,17 @@ pub fn StudentDemographicsPage() -> impl IntoView { // 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 data_map = RwSignal::new(HashMap::new()); + + let controller = use_student_form("demographics"); view! {
- + // Inputs here. impl IntoView { /> impl IntoView { // There must be a date of birth selection field here. impl IntoView { /> impl IntoView { /> impl IntoView { /> impl IntoView { impl IntoView { // TODO Make this a select dropdown. Date: Sat, 7 Feb 2026 12:32:48 -0500 Subject: [PATCH 41/56] chore: add small styling changes --- src/components/banner.rs | 2 +- src/components/multi_entry.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/banner.rs b/src/components/banner.rs index b6baa0c..3a4479e 100644 --- a/src/components/banner.rs +++ b/src/components/banner.rs @@ -39,7 +39,7 @@ pub fn Banner( }; view! { -
+
// Delete button
From caec4fb017a83d1dada94e4c5d2cccc045713d73 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Sat, 7 Feb 2026 12:49:29 -0500 Subject: [PATCH 42/56] chore: add Show and fix small typing errors --- src/pages/student/demographics_page.rs | 133 +++++++------- src/pages/student/extracurriculars_page.rs | 67 ++++---- src/pages/student/family_info_page.rs | 191 +++++++++++---------- src/pages/student/university_page.rs | 125 +++++++------- src/pages/student/work_experience_page.rs | 48 +++--- 5 files changed, 295 insertions(+), 269 deletions(-) diff --git a/src/pages/student/demographics_page.rs b/src/pages/student/demographics_page.rs index 641f410..2452423 100644 --- a/src/pages/student/demographics_page.rs +++ b/src/pages/student/demographics_page.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use crate::components::{OutlinedTextField, Select, TextFieldType, ValidatedForm}; +use crate::components::{Loading, OutlinedTextField, Select, TextFieldType, ValidatedForm}; use crate::pages::student::form_setup::use_student_form; #[component] @@ -12,68 +12,73 @@ pub fn StudentDemographicsPage() -> impl IntoView { let controller = use_student_form("demographics"); view! { -
-
- - // Inputs here. - - - // There must be a date of birth selection field here. - - - - - + +
+
+ } } diff --git a/src/pages/student/extracurriculars_page.rs b/src/pages/student/extracurriculars_page.rs index 7f16a2f..4c74f53 100644 --- a/src/pages/student/extracurriculars_page.rs +++ b/src/pages/student/extracurriculars_page.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use crate::components::{MultiEntry, OutlinedTextField, TextFieldType, ValidatedForm}; +use crate::components::{Loading, MultiEntry, OutlinedTextField, TextFieldType, ValidatedForm}; use crate::input; use crate::pages::student::form_setup::use_student_form; @@ -8,35 +8,40 @@ pub fn StudentExtracurricularsPage() -> impl IntoView { let controller = use_student_form("demographics"); view! { -
-
- - // Inputs here. - - - -
-
+ +
+
+ + // Inputs here. + + + +
+
+ } } \ No newline at end of file diff --git a/src/pages/student/family_info_page.rs b/src/pages/student/family_info_page.rs index 4fde2ac..0ce7e46 100644 --- a/src/pages/student/family_info_page.rs +++ b/src/pages/student/family_info_page.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use crate::components::{OutlinedTextField, TextFieldType, ValidatedForm}; +use crate::components::{Loading, OutlinedTextField, TextFieldType, ValidatedForm}; use crate::pages::student::form_setup::use_student_form; #[component] @@ -7,97 +7,102 @@ pub fn StudentFamilyPage() -> impl IntoView { let controller = use_student_form("family"); view! { -
-
- - // Inputs here. - - - - // TODO Could this be a select dropdown? - - // TODO Get a better example - - - - - // TODO Could this be a select dropdown? - - // TODO Get a better example - - - -
-
+ +
+
+ + // Inputs here. + + + + // TODO Could this be a select dropdown? + + // TODO Get a better example + + + + + // TODO Could this be a select dropdown? + + // TODO Get a better example + + + +
+
+ } } \ No newline at end of file diff --git a/src/pages/student/university_page.rs b/src/pages/student/university_page.rs index a7cb53e..8e2673e 100644 --- a/src/pages/student/university_page.rs +++ b/src/pages/student/university_page.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use crate::components::{OutlinedTextField, Select, TextFieldType, ValidatedForm}; +use crate::components::{Loading, OutlinedTextField, Select, TextFieldType, ValidatedForm}; use crate::pages::student::form_setup::use_student_form; #[component] @@ -10,64 +10,69 @@ pub fn StudentUniversityPage() -> impl IntoView { // major, study field, intended career, college_acceptance view! { -
-
- - // Inputs here. - - - // TODO Make this a select dropdown. - - - + // TODO Make this a select dropdown. + + +
+
+ } } diff --git a/src/pages/student/work_experience_page.rs b/src/pages/student/work_experience_page.rs index d600b43..21d6392 100644 --- a/src/pages/student/work_experience_page.rs +++ b/src/pages/student/work_experience_page.rs @@ -1,31 +1,37 @@ use std::collections::HashMap; use leptos::prelude::*; -use crate::components::{MultiEntry, ValidatedForm}; +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 data_map = RwSignal::new(HashMap::new()); + let controller = use_student_form("workexp"); view! { -
-
- - - -
-
+ +
+
+ + + +
+
+ } } \ No newline at end of file From 6a24c18d6f25d1b78e2dc6f40313caf1c5304370 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Mon, 9 Feb 2026 12:12:38 -0500 Subject: [PATCH 43/56] style: rustfmt run --- src/components/banner.rs | 12 ++---------- src/components/multi_entry.rs | 2 +- src/pages/student/demographics_page.rs | 5 +---- src/pages/student/extracurriculars_page.rs | 18 +++++++++++------- src/pages/student/family_info_page.rs | 5 +---- src/pages/student/university_page.rs | 10 ++-------- src/pages/student/work_experience_page.rs | 9 ++++----- 7 files changed, 22 insertions(+), 39 deletions(-) diff --git a/src/components/banner.rs b/src/components/banner.rs index 3a4479e..088e15d 100644 --- a/src/components/banner.rs +++ b/src/components/banner.rs @@ -42,21 +42,13 @@ pub fn Banner(
- icon + icon

{title.clone()}

-
+
impl IntoView { let controller = use_student_form("demographics"); view! { - +
diff --git a/src/pages/student/extracurriculars_page.rs b/src/pages/student/extracurriculars_page.rs index 4c74f53..b9ca521 100644 --- a/src/pages/student/extracurriculars_page.rs +++ b/src/pages/student/extracurriculars_page.rs @@ -8,13 +8,13 @@ pub fn StudentExtracurricularsPage() -> impl IntoView { let controller = use_student_form("demographics"); view! { - +
- + // Inputs here. impl IntoView { data_map=controller.data_map data_member="extracurricular" schema=vec![ - input!(Text, "activity_name", "Activity Name:", true, "Some activity..."), + 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!( + Text, "special_involvement", "Any special involvement:", false, "" + ), input!( Checkbox, "grades", "Grades Participated:", true, ["9th", "10th", "11th", "12th"] ), diff --git a/src/pages/student/family_info_page.rs b/src/pages/student/family_info_page.rs index 0ce7e46..49926af 100644 --- a/src/pages/student/family_info_page.rs +++ b/src/pages/student/family_info_page.rs @@ -7,10 +7,7 @@ pub fn StudentFamilyPage() -> impl IntoView { let controller = use_student_form("family"); view! { - +
diff --git a/src/pages/student/university_page.rs b/src/pages/student/university_page.rs index 8e2673e..01c6962 100644 --- a/src/pages/student/university_page.rs +++ b/src/pages/student/university_page.rs @@ -10,10 +10,7 @@ pub fn StudentUniversityPage() -> impl IntoView { // major, study field, intended career, college_acceptance view! { - +
@@ -56,10 +53,7 @@ pub fn StudentUniversityPage() -> impl IntoView { data_map=controller.data_map data_member="college_acceptance" required=true - value_list=vec![ - "Yes".to_string(), - "No".to_string() - ] + value_list=vec!["Yes".to_string(), "No".to_string()] /> // TODO Make this a select dropdown. impl IntoView { let controller = use_student_form("workexp"); view! { - +
@@ -22,7 +19,9 @@ pub fn StudentWorkExperiencePage() -> impl IntoView { data_map=controller.data_map data_member="extracurricular" schema=vec![ - input!(Text, "job_title", "Job Title:", true, "Waiter, Cashier, etc..."), + 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"), From da2cca33596d5927c1043e91616c6249eb8249f9 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Mon, 9 Feb 2026 12:13:01 -0500 Subject: [PATCH 44/56] chore: remove unused import --- src/pages/student/work_experience_page.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/student/work_experience_page.rs b/src/pages/student/work_experience_page.rs index e326609..deb0ef4 100644 --- a/src/pages/student/work_experience_page.rs +++ b/src/pages/student/work_experience_page.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; -use leptos::prelude::*; +use leptos::prelude::*; use crate::components::{Loading, MultiEntry, ValidatedForm}; use crate::input; use crate::pages::student::form_setup::use_student_form; From 494f93084f7b8552293c817434ffdf40edcac836 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Mon, 9 Feb 2026 12:14:16 -0500 Subject: [PATCH 45/56] chore: begin creation of scholarship export process, not done --- src/pages/admin/utils_page.rs | 111 ++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/src/pages/admin/utils_page.rs b/src/pages/admin/utils_page.rs index a923f74..7b8e7d1 100644 --- a/src/pages/admin/utils_page.rs +++ b/src/pages/admin/utils_page.rs @@ -52,10 +52,116 @@ fn get_important_dates() -> Vec { ] } +#[server] +async fn create_scholarship_export() -> Result, 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, + } + + 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::>>() + ); + 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::>>() + ); + 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>)>>) + 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::>() // This is a Vec + .iter().map(|val| val.clone().as_string().ok().flatten().unwrap_or_default()) + .collect::>(); + + // 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::>(); + + ScholarshipInfo { + name, + provider_name: provider_id, + relations: parsed_relations, + } + }) + .collect::>(); + + // 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::::new(); let create_dates = ServerAction::::new(); + let create_export = ServerAction::::new(); let on_click_comparisons = move |_| { create_comparisons.dispatch(CreateTestComparisons{}); @@ -65,6 +171,10 @@ pub fn AdminUtilsPage() -> impl IntoView { create_dates.dispatch(CreateDates{dates: get_important_dates()}); }; + let on_click_export = move |_| { + create_export.dispatch(CreateScholarshipExport{}); + }; + view! {
@@ -73,6 +183,7 @@ pub fn AdminUtilsPage() -> impl IntoView {
"Create Comparisons" "Create Dates" + "Scholarship Export"
}.into_any() From 4f61222f2d15709a3aafa3c1c13d4676ba54d720 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 08:18:31 -0500 Subject: [PATCH 46/56] chore: update service hours comparisons --- src/pages/api/comparisons.rs | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/pages/api/comparisons.rs b/src/pages/api/comparisons.rs index b9d3e26..6ff9624 100644 --- a/src/pages/api/comparisons.rs +++ b/src/pages/api/comparisons.rs @@ -283,13 +283,10 @@ fn make_comp_list() -> Vec { 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", @@ -297,13 +294,10 @@ fn make_comp_list() -> Vec { 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", @@ -311,13 +305,10 @@ fn make_comp_list() -> Vec { 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", From 97b83050682aea67b5c21eb53a3b9cd70b85589f Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 08:19:57 -0500 Subject: [PATCH 47/56] fix: don't panic on missing data --- src/common/comparison/base.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/common/comparison/base.rs b/src/common/comparison/base.rs index afca34b..6ba5c86 100644 --- a/src/common/comparison/base.rs +++ b/src/common/comparison/base.rs @@ -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 { - 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) -> Result { + 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()); } } From ffccaa6b0431422ef62282a3cc6e9951a49af65c Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 08:20:08 -0500 Subject: [PATCH 48/56] fix: allow parsing of float values --- src/common/comparison/number.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common/comparison/number.rs b/src/common/comparison/number.rs index 3440e97..9a16752 100644 --- a/src/common/comparison/number.rs +++ b/src/common/comparison/number.rs @@ -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::().unwrap(), + ValueType::Number(Some(num)) => num.parse::().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::().unwrap(), + ValueType::Number(Some(num)) => num.parse::().unwrap(), _ => { return Err("Target value is not a number".into()); } From d32261fe094342b18035186a56f3975ea5c6de1d Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 08:50:06 -0500 Subject: [PATCH 49/56] feat: create eligibility page --- src/app.rs | 1 + src/pages/api/students.rs | 55 ++++++-- src/pages/comparison_test.rs | 2 +- src/pages/student/athletics_page.rs | 2 +- src/pages/student/eligibility_page.rs | 175 ++++++++++++++++++++++++++ src/pages/student/mod.rs | 4 +- src/pages/student/shell.rs | 1 + 7 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 src/pages/student/eligibility_page.rs diff --git a/src/app.rs b/src/app.rs index 9960cae..a9be970 100644 --- a/src/app.rs +++ b/src/app.rs @@ -125,6 +125,7 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone { + diff --git a/src/pages/api/students.rs b/src/pages/api/students.rs index c965d56..da59e23 100644 --- a/src/pages/api/students.rs +++ b/src/pages/api/students.rs @@ -1,15 +1,23 @@ use leptos::prelude::*; +use leptos::server_fn::codec::Json; -#[server] +#[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; +} + +#[server(input = Json)] pub async fn put_student_data( subject: String, data_type: String, data_map: std::collections::HashMap ) -> Result<(), ServerFnError> { use leptos::logging::log; - use crate::utils::server::{create_dynamo_client, into_attr_map}; - use aws_sdk_dynamodb::types::AttributeValue; - use super::MAIN_TABLE_NAME; + use imports::*; let client = create_dynamo_client().await; @@ -40,11 +48,7 @@ pub async fn get_student_data( subject: String, data_type: String, ) -> Result, ServerFnError> { - use crate::utils::server::create_dynamo_client; - use std::collections::HashMap; - use crate::common::ValueType; - use aws_sdk_dynamodb::types::AttributeValue; - use super::MAIN_TABLE_NAME; + use imports::*; let client = create_dynamo_client().await; @@ -75,3 +79,36 @@ pub async fn get_student_data( }) .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, 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::>() + }) + .map_err(ServerFnError::from) +} diff --git a/src/pages/comparison_test.rs b/src/pages/comparison_test.rs index 50dd46f..28ce397 100644 --- a/src/pages/comparison_test.rs +++ b/src/pages/comparison_test.rs @@ -88,7 +88,7 @@ pub fn ComparisonTestPage() -> impl IntoView { _ => None, }; let comparison_outcome = match comp_opt { - Some(comp) => comp.compare(&student), + Some(comp) => comp.compare(&student.data), _ => Err("".to_string()), }; match comparison_outcome { diff --git a/src/pages/student/athletics_page.rs b/src/pages/student/athletics_page.rs index b311f1a..3c74f87 100644 --- a/src/pages/student/athletics_page.rs +++ b/src/pages/student/athletics_page.rs @@ -19,7 +19,7 @@ pub fn StudentAthleticsPage() -> impl IntoView { label="Athletics Activities:" description="Leave blank if not applicable." data_map=controller.data_map - data_member="athletic_participation" + data_member="sports_participation" schema=vec![ input!( Select, "sport_name", "Sport Name:", true, [ diff --git a/src/pages/student/eligibility_page.rs b/src/pages/student/eligibility_page.rs new file mode 100644 index 0000000..64ab926 --- /dev/null +++ b/src/pages/student/eligibility_page.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; +use leptos::logging::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! { +
+
+ "Checking eligibility..."
} + }> + {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(); + 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::>(); + + log!("Requirements list after conversion: {:?}", requirements_map); + + 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::>() + }) + .collect::>>(); + + log!("Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::>()); + + 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 { + log!("Scholarship is valid."); + Some(scholarship.clone()) + } else { + None + } + }) + .collect::>(); + + view! { +
{format!("Valid scholarships: {}", valid_list.len())}
+ +
{scholarship_name}
+ +
{scholarship_essay_prompt}
+
+
+ } + } + /> + }.into_any() + }} + +
+
+ } +} diff --git a/src/pages/student/mod.rs b/src/pages/student/mod.rs index 61275ce..5c3e5fa 100644 --- a/src/pages/student/mod.rs +++ b/src/pages/student/mod.rs @@ -9,6 +9,7 @@ mod work_experience_page; mod university_page; mod family_info_page; mod form_setup; +mod eligibility_page; pub use shell::*; pub use home_page::*; @@ -19,4 +20,5 @@ pub use academics_page::*; pub use extracurriculars_page::*; pub use work_experience_page::*; pub use university_page::*; -pub use family_info_page::*; \ No newline at end of file +pub use family_info_page::*; +pub use eligibility_page::*; \ No newline at end of file diff --git a/src/pages/student/shell.rs b/src/pages/student/shell.rs index ee33082..ba0c9d6 100644 --- a/src/pages/student/shell.rs +++ b/src/pages/student/shell.rs @@ -38,6 +38,7 @@ pub fn StudentShell() -> impl IntoView { ], ), ), + TabInfo::new("Eligible Scholarships", "eligibility", None) ] /> From bbc19b88be4b3a3854509047498468e64fbda9c6 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 08:50:35 -0500 Subject: [PATCH 50/56] fix: force use of json in server functions --- src/pages/api/providers.rs | 3 +- src/pages/api/scholarships.rs | 63 +++++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/pages/api/providers.rs b/src/pages/api/providers.rs index 76964bc..3cb0893 100644 --- a/src/pages/api/providers.rs +++ b/src/pages/api/providers.rs @@ -1,6 +1,7 @@ use leptos::prelude::*; use leptos::logging::log; use std::collections::HashMap; +use leptos::server_fn::codec::Json; use crate::common::ValueType; #[cfg(feature = "ssr")] @@ -45,7 +46,7 @@ pub async fn get_provider_contact(id: String) -> Result) -> Result<(), ServerFnError> { use aws_sdk_dynamodb::error::ProvideErrorMetadata; diff --git a/src/pages/api/scholarships.rs b/src/pages/api/scholarships.rs index 0e5dd5a..161058f 100644 --- a/src/pages/api/scholarships.rs +++ b/src/pages/api/scholarships.rs @@ -1,27 +1,27 @@ #[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::utils::server::{create_dynamo_client, into_attr_map}; -#[cfg(feature = "ssr")] -use crate::common::ValueType; +mod imports { + 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, into_attr_map}; + pub use crate::common::ValueType; +} use crate::common::ExpandableInfo; use leptos::prelude::ServerFnError; use leptos::server; - +use leptos::server_fn::codec::Json; #[cfg(feature = "ssr")] use super::SCHOLARSHIPS_TABLE; #[server(GetScholarshipInfo)] pub async fn get_scholarship_info(id: String) -> Result { + use imports::*; + let client = create_dynamo_client().await; // Perform the operation - we just want to return all data that's contained in this entry, @@ -50,8 +50,9 @@ pub async fn get_scholarship_info(id: String) -> Result Result<(), ServerFnError> { + use imports::*; use aws_sdk_dynamodb::error::ProvideErrorMetadata; let client = create_dynamo_client().await; @@ -75,6 +76,8 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF #[server(GetAllScholarshipInfo)] pub async fn get_all_scholarship_info() -> Result, ServerFnError> { + use imports::*; + let client = create_dynamo_client().await; log!("Getting all scholarship info"); match client @@ -100,6 +103,8 @@ pub async fn get_all_scholarship_info() -> Result, ServerFnE /// Gets all scholarships that are associated with the given scholarship provider's ID. #[server(GetProviderScholarships)] pub async fn get_provider_scholarships(provider_id: String) -> Result, ServerFnError> { + use imports::*; + let client = create_dynamo_client().await; log!("Getting provider scholarships for provider with ID {:?}", provider_id); @@ -129,6 +134,8 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result Result { + use imports::*; + let client = create_dynamo_client().await; log!("Creating scholarship for provider with ID {:?}", provider_id); @@ -173,6 +180,8 @@ pub async fn register_scholarship(provider_id: String) -> Result Result<(), ServerFnError> { + use imports::*; + let client = create_dynamo_client().await; log!("Deleting scholarship with ID {:?} for provider with ID {:?}", scholarship_id, provider_id); @@ -197,3 +206,29 @@ pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: St } } } + +#[server] +pub async fn get_all_scholarships() -> Result>, 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::>() + }).collect::>() + }) + .map_err(ServerFnError::from) +} From f7d66ccd8e607f600c2bab5213c10033bcdd0141 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 08:51:06 -0500 Subject: [PATCH 51/56] chore: remove unused import --- src/pages/student/eligibility_page.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/student/eligibility_page.rs b/src/pages/student/eligibility_page.rs index 64ab926..9ada18c 100644 --- a/src/pages/student/eligibility_page.rs +++ b/src/pages/student/eligibility_page.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use leptos::logging::log; use leptos::prelude::*; use crate::common::{ComparisonData, ExpandableInfo, ValueType}; From 7d02c6505eb61b2bc84613a6bc0e03c1b08b4c43 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 08:55:29 -0500 Subject: [PATCH 52/56] chore: tidy several logs to be debug-only --- src/pages/api/providers.rs | 4 ++-- src/pages/api/scholarships.rs | 15 +++++++-------- src/pages/api/students.rs | 4 ++-- src/pages/student/eligibility_page.rs | 10 ++++------ 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/pages/api/providers.rs b/src/pages/api/providers.rs index 3cb0893..988e932 100644 --- a/src/pages/api/providers.rs +++ b/src/pages/api/providers.rs @@ -1,5 +1,5 @@ use leptos::prelude::*; -use leptos::logging::log; +use leptos::logging::debug_log; use std::collections::HashMap; use leptos::server_fn::codec::Json; use crate::common::ValueType; @@ -17,7 +17,7 @@ pub async fn get_provider_contact(id: String) -> Result Result { @@ -79,7 +78,7 @@ pub async fn get_all_scholarship_info() -> Result, ServerFnE use imports::*; let client = create_dynamo_client().await; - log!("Getting all scholarship info"); + debug_log!("Getting all scholarship info"); match client .scan() .table_name(SCHOLARSHIPS_TABLE) @@ -107,7 +106,7 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result Result ) -> Result<(), ServerFnError> { - use leptos::logging::log; use imports::*; let client = create_dynamo_client().await; @@ -31,7 +31,7 @@ pub async fn put_student_data( AttributeValue::S(data_type.to_uppercase()) ); - log!("Inserting this item: {:?}", data_map_attr); + debug_log!("Inserting this item: {:?}", data_map_attr); client .put_item() diff --git a/src/pages/student/eligibility_page.rs b/src/pages/student/eligibility_page.rs index 9ada18c..e472472 100644 --- a/src/pages/student/eligibility_page.rs +++ b/src/pages/student/eligibility_page.rs @@ -1,4 +1,4 @@ -use leptos::logging::log; +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}; @@ -66,7 +66,7 @@ pub fn StudentEligibilityPage() -> impl IntoView { let scholarship_name = scholarship.data.get("name") .unwrap_or(&ValueType::String(None)) .as_string().ok().flatten().unwrap_or_default(); - log!("Checking scholarship: {}", scholarship_name); + 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 @@ -78,8 +78,6 @@ pub fn StudentEligibilityPage() -> impl IntoView { .unwrap_or_default() .values().cloned() .collect::>(); - - log!("Requirements list after conversion: {:?}", requirements_map); let resolved_requirements = requirements_map .iter() @@ -97,7 +95,7 @@ pub fn StudentEligibilityPage() -> impl IntoView { }) .collect::>>(); - log!("Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::>()); + debug_log!("Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::>()); let valid = resolved_requirements.iter() .all(|list| { @@ -110,7 +108,7 @@ pub fn StudentEligibilityPage() -> impl IntoView { }); if valid { - log!("Scholarship is valid."); + debug_log!("Scholarship is valid."); Some(scholarship.clone()) } else { None From 637f143ac1d829cce21fd8ce5e5fc57a5d8a15d8 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 09:04:28 -0500 Subject: [PATCH 53/56] chore: tidy several logs to be debug-only --- src/pages/scholarship_info.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index 54e438b..ad7ccef 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use leptos::either::Either; use leptos::html::Dialog; -use leptos::logging::log; +use leptos::logging::{debug_log, log}; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; use leptos_router::hooks::{use_navigate, use_params}; @@ -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()); }; @@ -179,7 +179,7 @@ fn ScholarshipList( .get() .map(|s| s.subject.clone()) .unwrap_or_default(); - log!("Deleting scholarship with subject {}", subject); + debug_log!("Deleting scholarship with subject {}", subject); delete_action .dispatch(DeleteProviderScholarship { scholarship_id: subject, @@ -385,10 +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") { - log!("Found requirements map: {:?}", map); + debug_log!("Found requirements map: {:?}", map); map.clone() } else { - log!("Couldn't find requirements map in scholarship info. Using empty map."); + debug_log!("Couldn't find requirements map in scholarship info. Using empty map."); HashMap::new() }; @@ -496,7 +496,7 @@ fn ScholarshipForm( 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 }); @@ -505,7 +505,7 @@ fn ScholarshipForm( Effect::watch( move || submit_action.value().get(), move |value, _, _| { - log!("Running submit_action effect"); + debug_log!("Running submit_action effect"); let Some(result) = value else { return; }; From 17ee7bdc4bc76e9d45727624d71effeeccf5f418 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 09:04:42 -0500 Subject: [PATCH 54/56] chore: change placeholder to be more provider specific --- src/pages/provider_contact.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/provider_contact.rs b/src/pages/provider_contact.rs index b560cf6..696d33a 100644 --- a/src/pages/provider_contact.rs +++ b/src/pages/provider_contact.rs @@ -120,7 +120,7 @@ pub fn ProviderContactPage() -> impl IntoView { Date: Tue, 10 Feb 2026 09:05:15 -0500 Subject: [PATCH 55/56] chore: remove unused import --- src/pages/scholarship_info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index ad7ccef..cae24f4 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use leptos::either::Either; use leptos::html::Dialog; -use leptos::logging::{debug_log, log}; +use leptos::logging::debug_log; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; use leptos_router::hooks::{use_navigate, use_params}; From 1f51357ca5075bfd7cddc7543b837ed97f201fe7 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 10 Feb 2026 09:22:12 -0500 Subject: [PATCH 56/56] chore: remove admin routes --- src/app.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index a9be970..bc50b33 100644 --- a/src/app.rs +++ b/src/app.rs @@ -65,7 +65,6 @@ pub fn AppWithRoutes() -> impl IntoView { // -