From 7645ecc2402ed2da07182a847d29b45000232c59 Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 5 Dec 2025 11:59:22 -0500 Subject: [PATCH 01/32] Framework for date component design --- src/components/date.rs | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/components/date.rs diff --git a/src/components/date.rs b/src/components/date.rs new file mode 100644 index 0000000..a73d79e --- /dev/null +++ b/src/components/date.rs @@ -0,0 +1,47 @@ +use leptos::prelude::*; + +/// # Date Component +/// +/// +/// Example usage: +/// ``` + +/// ``` +#[component] +pub fn Date( + /// The main label displayed on the button. + #[prop(into)] + title: String, + /// The calendar date of the event + #[prop(into)] + date: String, + /// A brief explanatory text that describes the destination page. + #[prop(into)] + description: String, + /// The file path or URL of the icon displayed at the top of the button. + #[prop(into)] + icon: String, + // The status of the event (open, upcoming, deadline...) + //#[prop(into)] status: String, +) -> impl IntoView { + view! { +
+
+ icon +
+
+

+ {title.clone()} +

+

+ {date.clone()} +

+

+ {description.clone()} +

+
+
+ } +} From d5659f6da76fa5ad8aab1ea7a9ad6452058b8fd1 Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 5 Dec 2025 11:59:30 -0500 Subject: [PATCH 02/32] Added to mod.rs --- src/components/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/mod.rs b/src/components/mod.rs index f0297f7..58f0fc5 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -2,6 +2,7 @@ mod action_button; mod checkbox; mod chips; mod dashboard_button; +mod date; mod loading; mod multi_entry; mod panel; @@ -12,6 +13,6 @@ mod text_field; mod utils; pub use self::{ - action_button::*, checkbox::*, chips::*, dashboard_button::*, loading::*, multi_entry::*, - panel::*, radio::*, row::*, select::*, text_field::*, + action_button::*, checkbox::*, chips::*, dashboard_button::*, date::*, loading::*, + multi_entry::*, panel::*, radio::*, row::*, select::*, text_field::*, }; From 3b1bf8856792dde3967d4563c247978c536f41a9 Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 5 Dec 2025 11:59:39 -0500 Subject: [PATCH 03/32] Test in frontend --- src/pages/test_page.rs | 77 +++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index f3e1f28..2483330 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -1,28 +1,65 @@ -use crate::components::DashboardButton; +use crate::components::{DashboardButton, Date}; use leptos::prelude::*; #[component] pub fn TestPage() -> impl IntoView { view! { -
- - - +
+
+
+
+ + + +
+
+
+
+

Calendar

+
+
+
+ + + +
+
+
+ +
+
+
} } From 4115d24bd88344d5b90de9be6e8c1c79b67e5923 Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 5 Dec 2025 12:39:41 -0500 Subject: [PATCH 04/32] Added status to date --- src/components/date.rs | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/components/date.rs b/src/components/date.rs index a73d79e..9e57fff 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -21,8 +21,9 @@ pub fn Date( /// The file path or URL of the icon displayed at the top of the button. #[prop(into)] icon: String, - // The status of the event (open, upcoming, deadline...) - //#[prop(into)] status: String, + /// The status of the event (open, upcoming, deadline...) + #[prop(into)] + status: String, ) -> impl IntoView { view! {
icon
-
-

- {title.clone()} -

-

- {date.clone()} -

-

- {description.clone()} -

+
+
+
+

+ {title.clone()} +

+

+ {date.clone()} +

+

+ {description.clone()} +

+
+
+ + {status.clone()} + +
+
} From bbeb713fd658cd77c51fed48d5394a8c48a62a7e Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 5 Dec 2025 12:40:12 -0500 Subject: [PATCH 05/32] Pictures need to start with "/" --- src/pages/test_page.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index 2483330..36b0f6f 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -11,19 +11,19 @@ pub fn TestPage() -> impl IntoView { @@ -39,24 +39,27 @@ pub fn TestPage() -> impl IntoView { title="Form opens" date="November 15, 2025" description="Edit user profile" - icon="Check_Style2.png" + icon="/Check_Style2.png" + status="Open" />
- From 5ad00e69af87d468780f474d5fe2cefb7e7c7a19 Mon Sep 17 00:00:00 2001 From: meraghon Date: Tue, 9 Dec 2025 11:51:28 -0500 Subject: [PATCH 06/32] Create struct file --- src/common/date.rs | 1 + src/common/mod.rs | 1 + 2 files changed, 2 insertions(+) create mode 100644 src/common/date.rs diff --git a/src/common/date.rs b/src/common/date.rs new file mode 100644 index 0000000..d0c66a0 --- /dev/null +++ b/src/common/date.rs @@ -0,0 +1 @@ +pub struct DateInfo {} diff --git a/src/common/mod.rs b/src/common/mod.rs index fd320cb..c888898 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,3 +1,4 @@ +mod date; mod expandable_info; mod form_schema; mod user; From 7abb1c369f116234fe2cf8d003cf6f049424d630 Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 12 Dec 2025 12:13:25 -0500 Subject: [PATCH 07/32] Create DateInfo struct --- src/common/date_info.rs | 11 +++++++++++ src/common/mod.rs | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/common/date_info.rs diff --git a/src/common/date_info.rs b/src/common/date_info.rs new file mode 100644 index 0000000..d74a949 --- /dev/null +++ b/src/common/date_info.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DateInfo { + #[serde()] + pub(crate) title: String, + #[serde(rename = "date")] + pub(crate) date: String, + #[serde(rename = "desc")] + pub(crate) description: String, +} diff --git a/src/common/mod.rs b/src/common/mod.rs index c888898..2a3b734 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,7 +1,8 @@ mod date; +mod date_info; mod expandable_info; mod form_schema; mod user; pub mod value_type; -pub use self::{expandable_info::*, form_schema::*, user::*, value_type::*}; +pub use self::{date_info::*, expandable_info::*, form_schema::*, user::*, value_type::*}; From e3ea13d07d7a186dba88e50b6226856b77ed168b Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 12 Dec 2025 12:13:41 -0500 Subject: [PATCH 08/32] Implement date element with struct --- src/components/date.rs | 74 ++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/src/components/date.rs b/src/components/date.rs index 9e57fff..8166a5a 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -1,5 +1,5 @@ +use crate::common::DateInfo; use leptos::prelude::*; - /// # Date Component /// /// @@ -9,15 +9,9 @@ use leptos::prelude::*; /// ``` #[component] pub fn Date( - /// The main label displayed on the button. + /// Struct of title, date, and description #[prop(into)] - title: String, - /// The calendar date of the event - #[prop(into)] - date: String, - /// A brief explanatory text that describes the destination page. - #[prop(into)] - description: String, + important_dates: Vec, /// The file path or URL of the icon displayed at the top of the button. #[prop(into)] icon: String, @@ -25,35 +19,39 @@ pub fn Date( #[prop(into)] status: String, ) -> impl IntoView { - view! { -
-
- icon -
-
-
-
-

- {title.clone()} -

-

- {date.clone()} -

-

- {description.clone()} -

-
-
- - {status.clone()} - + important_dates.into_iter() + .map(|info: DateInfo| { + view! { +
+
+ icon +
+
+
+
+

+ {info.title} +

+

+ {info.date} +

+

+ {info.description} +

+
+
+ + {status.clone()} + +
-
-
- } +
+ } + }) + .collect_view() } From d43a6ed15309cffa8ddc6eb752b5dd94905c13b4 Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 12 Dec 2025 12:13:55 -0500 Subject: [PATCH 09/32] Test date element with hard-coded struct info --- src/pages/test_page.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index 36b0f6f..e52ef2a 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -1,3 +1,4 @@ +use crate::common::DateInfo; use crate::components::{DashboardButton, Date}; use leptos::prelude::*; @@ -31,30 +32,30 @@ pub fn TestPage() -> impl IntoView {
-

Calendar

+

Important Dates

-
From 38a0482b8cd2ca8de1808a22fd555a25cd5aa26b Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 19 Dec 2025 10:27:43 -0500 Subject: [PATCH 10/32] Date range enum --- src/common/date_info.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/date_info.rs b/src/common/date_info.rs index d74a949..2601a3a 100644 --- a/src/common/date_info.rs +++ b/src/common/date_info.rs @@ -5,7 +5,13 @@ pub struct DateInfo { #[serde()] pub(crate) title: String, #[serde(rename = "date")] - pub(crate) date: String, + pub(crate) date: DateRange, #[serde(rename = "desc")] pub(crate) description: String, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DateRange { + Single(String), + Range(String, String), +} From fab172c4d1d183ac4fc058ed369f6a0cd8095fb3 Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 19 Dec 2025 10:28:07 -0500 Subject: [PATCH 11/32] Implement function for date range enum in date.rs --- src/components/date.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/date.rs b/src/components/date.rs index 8166a5a..798e739 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -1,4 +1,5 @@ -use crate::common::DateInfo; +use crate::common::{DateInfo, DateRange}; +use leptos::either::Either; use leptos::prelude::*; /// # Date Component /// @@ -7,6 +8,18 @@ use leptos::prelude::*; /// ``` /// ``` + +fn render_dates(dates: DateRange) -> impl IntoView { + match dates { + DateRange::Single(date) => Either::Left(view! { +

{date}

+ }), + DateRange::Range(start_date, end_date) => Either::Right(view! { +

{start_date} " - " {end_date}

+ }), + } +} + #[component] pub fn Date( /// Struct of title, date, and description @@ -31,11 +44,11 @@ pub fn Date(
-

+

{info.title}

-

- {info.date} +

+ {render_dates(info.date)}

{info.description} From 5e9e9bcf58cab0c065fe3390b3158392a08654ed Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 19 Dec 2025 10:28:19 -0500 Subject: [PATCH 12/32] Test date range enum --- src/pages/test_page.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index e52ef2a..3e8c4cb 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -1,4 +1,4 @@ -use crate::common::DateInfo; +use crate::common::{DateInfo, DateRange}; use crate::components::{DashboardButton, Date}; use leptos::prelude::*; @@ -40,7 +40,7 @@ pub fn TestPage() -> impl IntoView { important_dates=vec!{ DateInfo { title: "Title".to_string(), - date: "Nov 4".to_string(), + date: DateRange::Range("February 2nd, 2026".to_string(), "February 10th, 2026".to_string()), description: "This is a test".to_string(), } } @@ -51,7 +51,7 @@ pub fn TestPage() -> impl IntoView { important_dates=vec!{ DateInfo { title: "Scholarships Due".to_string(), - date: "Nov 4".to_string(), + date: DateRange::Single("November 4th, 2025".to_string()), description: "This is a test".to_string(), } } From 03f7adb74ddf97c970bf0107b3a6585af4eb0601 Mon Sep 17 00:00:00 2001 From: meraghon Date: Mon, 22 Dec 2025 10:36:07 -0500 Subject: [PATCH 13/32] Created DateStatus enum --- src/common/date_info.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/common/date_info.rs b/src/common/date_info.rs index 2601a3a..0cc39bb 100644 --- a/src/common/date_info.rs +++ b/src/common/date_info.rs @@ -8,6 +8,8 @@ pub struct DateInfo { pub(crate) date: DateRange, #[serde(rename = "desc")] pub(crate) description: String, + #[serde(rename = "stat")] + pub(crate) status: DateStatus, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -15,3 +17,10 @@ pub enum DateRange { Single(String), Range(String, String), } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DateStatus { + Upcoming, + Open, + Deadline, + Closed, +} From 7cf2facb182e8cf98f9059e28e056aa396feb823 Mon Sep 17 00:00:00 2001 From: meraghon Date: Mon, 22 Dec 2025 10:36:56 -0500 Subject: [PATCH 14/32] function renders status of each state --- src/components/date.rs | 94 ++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/src/components/date.rs b/src/components/date.rs index 798e739..ec54de1 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -1,5 +1,5 @@ -use crate::common::{DateInfo, DateRange}; -use leptos::either::Either; +use crate::common::{DateInfo, DateRange, DateStatus}; +use leptos::either::{Either, EitherOf4}; use leptos::prelude::*; /// # Date Component /// @@ -20,6 +20,34 @@ fn render_dates(dates: DateRange) -> impl IntoView { } } +fn render_status(status: DateStatus) -> impl IntoView { + match status { + DateStatus::Upcoming => EitherOf4::A(view! { + + Upcoming + + }), + DateStatus::Open => EitherOf4::B(view! { + + Open + + }), + DateStatus::Deadline => EitherOf4::C(view! { + + Deadline + + }), + DateStatus::Closed => EitherOf4::D(view! { + + Closed + + }), + } +} #[component] pub fn Date( /// Struct of title, date, and description @@ -28,43 +56,37 @@ pub fn Date( /// The file path or URL of the icon displayed at the top of the button. #[prop(into)] icon: String, - /// The status of the event (open, upcoming, deadline...) - #[prop(into)] - status: String, ) -> impl IntoView { - important_dates.into_iter() - .map(|info: DateInfo| { - view! { -

-
- icon -
-
-
-
-

- {info.title} -

-

- {render_dates(info.date)} -

-

- {info.description} -

-
-
- - {status.clone()} - -
+
+ icon +
+
+
+
+

+ {info.title} +

+

+ {render_dates(info.date)} +

+

+ {info.description} +

+
+
+ {render_status(info.status)}
-
- } - }) - .collect_view() +
+
+ } + }) + .collect_view() } From 24c20f419931494f3048fc3b856f0fd8cd4f9391 Mon Sep 17 00:00:00 2001 From: meraghon Date: Mon, 22 Dec 2025 10:37:05 -0500 Subject: [PATCH 15/32] deleted unused file --- src/common/date.rs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/common/date.rs diff --git a/src/common/date.rs b/src/common/date.rs deleted file mode 100644 index d0c66a0..0000000 --- a/src/common/date.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct DateInfo {} From 53fd3a56c4ca59f9f1a0a762b59f31819883c5cf Mon Sep 17 00:00:00 2001 From: meraghon Date: Mon, 22 Dec 2025 11:40:52 -0500 Subject: [PATCH 16/32] add example usages for Single and Range --- src/components/date.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/components/date.rs b/src/components/date.rs index ec54de1..6f91749 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -4,9 +4,33 @@ use leptos::prelude::*; /// # Date Component /// /// -/// Example usage: +/// Example usage - Single Date: /// ``` - +/// +/// ``` +/// Example usage - Date Range +/// ``` +/// /// ``` fn render_dates(dates: DateRange) -> impl IntoView { From bb9eb465f4361c9724fccf014352e4fb712679c1 Mon Sep 17 00:00:00 2001 From: meraghon Date: Fri, 2 Jan 2026 15:30:40 -0500 Subject: [PATCH 17/32] Display date status --- src/pages/test_page.rs | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index 3e8c4cb..800e6a7 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -1,4 +1,4 @@ -use crate::common::{DateInfo, DateRange}; +use crate::common::{DateInfo, DateRange, DateStatus}; use crate::components::{DashboardButton, Date}; use leptos::prelude::*; @@ -42,10 +42,11 @@ pub fn TestPage() -> impl IntoView { title: "Title".to_string(), date: DateRange::Range("February 2nd, 2026".to_string(), "February 10th, 2026".to_string()), description: "This is a test".to_string(), + status: DateStatus::Upcoming } } icon="/Check_Style2.png" - status="Open" + /> impl IntoView { title: "Scholarships Due".to_string(), date: DateRange::Single("November 4th, 2025".to_string()), description: "This is a test".to_string(), + status: DateStatus::Open + } + } + icon="/Warning_Style2.png" + /> + +
From e143149dac5d039d1628f2b432a4e3909af19af3 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Wed, 14 Jan 2026 11:38:53 -0500 Subject: [PATCH 18/32] merge: add missing import --- src/app.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app.rs b/src/app.rs index 3bd0973..24d92f9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,7 @@ use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_oidc::{Auth, AuthParameters, AuthSignal, Challenge}; use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path}; use crate::components::ToastList; +use leptos::logging::log; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { From 6fdf7f01137f99c8b8b12dc0090214b5b89ae588 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Wed, 14 Jan 2026 12:56:32 -0500 Subject: [PATCH 19/32] feat: add improvements to the date component --- src/components/date.rs | 179 +++++++++++++++++++++-------------------- src/pages/test_page.rs | 31 ++----- 2 files changed, 98 insertions(+), 112 deletions(-) diff --git a/src/components/date.rs b/src/components/date.rs index 6f91749..151e0e7 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -1,116 +1,119 @@ use crate::common::{DateInfo, DateRange, DateStatus}; -use leptos::either::{Either, EitherOf4}; use leptos::prelude::*; -/// # Date Component -/// -/// -/// Example usage - Single Date: -/// ``` -/// -/// ``` -/// Example usage - Date Range -/// ``` -/// -/// ``` fn render_dates(dates: DateRange) -> impl IntoView { - match dates { - DateRange::Single(date) => Either::Left(view! { -

{date}

- }), - DateRange::Range(start_date, end_date) => Either::Right(view! { -

{start_date} " - " {end_date}

- }), + let date_text = match dates { + DateRange::Single(date) => date, + DateRange::Range(start_date, end_date) => + format!("{} - {}", start_date, end_date), + }; + + view! { + {move || date_text.clone()} } } fn render_status(status: DateStatus) -> impl IntoView { - match status { - DateStatus::Upcoming => EitherOf4::A(view! { - - Upcoming - - }), - DateStatus::Open => EitherOf4::B(view! { - - Open - - }), - DateStatus::Deadline => EitherOf4::C(view! { - - Deadline - - }), - DateStatus::Closed => EitherOf4::D(view! { - - Closed - - }), + let status_text = match status { + DateStatus::Upcoming => "Upcoming", + DateStatus::Open => "Open", + DateStatus::Deadline => "Deadline", + DateStatus::Closed => "Closed", + }; + + view! { + + {move || status_text} + } } + +/// # Date Component +/// A single date, for use in a [`DateList`] component. +/// +/// Example usage: +/// ``` +/// let date_info = DateInfo { +/// title: "Example Title".to_string(), +/// date: DateRange::Single("January 1st, 1990".to_string()), +/// description: "A description of the date.".to_string(), +/// status: DateStatus::Upcoming +/// } +/// +/// view! { +/// +/// } +/// ``` #[component] -pub fn Date( +fn Date( /// Struct of title, date, and description - #[prop(into)] - important_dates: Vec, - /// The file path or URL of the icon displayed at the top of the button. - #[prop(into)] - icon: String, + #[prop(into)] info: Signal, ) -> impl IntoView { - important_dates - .into_iter() - .map(|info: DateInfo| { - view! { -
-
- icon -
+ view! { +
+ //
+ // icon + //

- {info.title} + {info.get().title}

- {render_dates(info.date)} + {move || render_dates(info.get().date)}

- {info.description} + {info.get().description}

- {render_status(info.status)} + {move || render_status(info.get().status)}
-
- } - }) - .collect_view() +
+ } +} + +///# Dates List Component +/// Displays a list of important dates in a column, with badges that draw attention to dates that +/// are more important. +/// +/// Example usage: +/// ``` +/// let dates = vec![ +/// DateInfo { +/// title: "Example Title".to_string(), +/// date: DateRange::Single("January 1st, 1990".to_string()), +/// description: "A description of the date.".to_string(), +/// status: DateStatus::Upcoming +/// } +/// ] +/// +/// view! { +/// +/// } +/// ``` +#[component] +pub fn DateList( + #[prop(into)] dates: Vec, +) -> impl IntoView { + view! { + {move || { + dates.iter() + .map(|info| { + view! { + + } + }).collect_view() + }} + } } diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index 800e6a7..2d16376 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -1,5 +1,5 @@ use crate::common::{DateInfo, DateRange, DateStatus}; -use crate::components::{DashboardButton, Date}; +use crate::components::{DashboardButton, DateList}; use leptos::prelude::*; #[component] @@ -36,50 +36,33 @@ pub fn TestPage() -> impl IntoView {
- - - -
From 9eacc9a61829bcdf8e7f40f5a05b62db5a943347 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 15 Jan 2026 00:54:55 -0500 Subject: [PATCH 20/32] temp: pc change (add server function and date comparison) --- src/components/date.rs | 20 +++++++++++++++----- src/pages/test_page.rs | 9 ++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/components/date.rs b/src/components/date.rs index 151e0e7..f2b7fb5 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -21,10 +21,20 @@ fn render_status(status: DateStatus) -> impl IntoView { DateStatus::Closed => "Closed", }; + let background_class = match status { + DateStatus::Upcoming => "bg-yellow-100 text-yellow-800 border-yellow-100", + DateStatus::Open => "bg-green-100 text-green-800 border-green-100", + DateStatus::Deadline | DateStatus::Closed => "bg-red-100 text-red-800 border-red-100", + }; + + let classes = + format!("inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs + font-medium w-fit whitespace-nowrap shrink-0 {}", + background_class + ); + view! { - + {move || status_text} } @@ -54,8 +64,8 @@ fn Date( #[prop(into)] info: Signal, ) -> impl IntoView { view! { -
+
//
// icon //
diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index 2d16376..f288a7f 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -29,13 +29,12 @@ pub fn TestPage() -> impl IntoView { />
-
-
+
+

Important Dates

-
-
+
+
Date: Thu, 15 Jan 2026 07:30:32 -0500 Subject: [PATCH 21/32] style: automatic rustfmt run --- src/app.rs | 16 +-- src/components/date.rs | 38 ++---- src/components/lists/multi/checkbox.rs | 27 +++-- src/components/lists/multi/chips.rs | 106 +++++++++-------- src/components/lists/single/radio.rs | 17 ++- src/components/lists/single/select.rs | 25 ++-- src/components/text_field.rs | 14 +-- src/pages/comparison_test.rs | 157 ++++++++++++------------- src/pages/home_page.rs | 13 +- src/pages/loaner_page.rs | 131 +++++++++++---------- src/pages/provider_contact.rs | 2 +- src/pages/test_page.rs | 63 +++++----- 12 files changed, 309 insertions(+), 300 deletions(-) diff --git a/src/app.rs b/src/app.rs index 24d92f9..9549173 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,10 +32,10 @@ pub fn App() -> impl IntoView { view! { // injects a stylesheet into the document // id=leptos means cargo-leptos will hot-reload this stylesheet - + // sets the document title - + <Title text="R15 Scholarship App DEV" /> // content for this welcome page <Router> @@ -102,15 +102,15 @@ pub fn AppWithRoutes() -> impl IntoView { <ToastList> // TODO Create a 404 page <Routes fallback=|| "Page not found.".into_view()> - <Route path=path!("") view=HomePage/> - <Route path=path!("/about") view=AboutPage/> - <Route path=path!("/test_page") view=TestPage/> + <Route path=path!("") view=HomePage /> + <Route path=path!("/about") view=AboutPage /> + <Route path=path!("/test_page") view=TestPage /> <Route path=path!("/comparison") view=ComparisonTestPage /> - <Route path=path!("/providers") view=ProviderPortal/> + <Route path=path!("/providers") view=ProviderPortal /> <Route path=path!("/providers/profile") view=ProviderContactPage /> <ParentRoute path=path!("/providers/scholarships") view=ScholarshipInfoPage> - <Route path=path!(":id") view=ScholarshipInfoPage/> - <Route path=path!("") view=ScholarshipInfoPage/> + <Route path=path!(":id") view=ScholarshipInfoPage /> + <Route path=path!("") view=ScholarshipInfoPage /> </ParentRoute> <ParentRoute path=path!("loaners") view=LoanerShell> <Route path=path!("") view=LoanerFallback /> diff --git a/src/components/date.rs b/src/components/date.rs index f2b7fb5..103c8a9 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -8,9 +8,7 @@ fn render_dates(dates: DateRange) -> impl IntoView { format!("{} - {}", start_date, end_date), }; - view! { - <span>{move || date_text.clone()}</span> - } + view! { <span>{move || date_text.clone()}</span> } } fn render_status(status: DateStatus) -> impl IntoView { @@ -33,11 +31,7 @@ fn render_status(status: DateStatus) -> impl IntoView { background_class ); - view! { - <span class=classes> - {move || status_text} - </span> - } + view! { <span class=classes>{move || status_text}</span> } } /// # Date Component @@ -65,26 +59,20 @@ fn Date( ) -> impl IntoView { view! { <div class="flex items-start gap-3 rounded-lg border-grey-300 p-3 - w-full text-left shadow-lg"> + w-full text-left shadow-lg"> // <div class="flex-shrink-0 mt-0.5"> - // <img src={icon} class="h-4 w-4" alt="icon"/> + // <img src={icon} class="h-4 w-4" alt="icon"/> // </div> <div class="flex-1 min-w-0"> <div class="flex items-start justify-between"> <div class="flex-1"> - <h3 class="font-bold text-md text-base"> - {info.get().title} - </h3> + <h3 class="font-bold text-md text-base">{info.get().title}</h3> <h3 class="text-md text-base mt-1"> {move || render_dates(info.get().date)} </h3> - <p class="text-sm text-gray-600"> - {info.get().description} - </p> - </div> - <div class="flex-shrink-0 ml-2"> - {move || render_status(info.get().status)} + <p class="text-sm text-gray-600">{info.get().description}</p> </div> + <div class="flex-shrink-0 ml-2">{move || render_status(info.get().status)}</div> </div> </div> </div> @@ -116,14 +104,12 @@ pub fn DateList( ) -> impl IntoView { view! { {move || { - dates.iter() + dates + .iter() .map(|info| { - view! { - <Date - info=info.clone() - /> - } - }).collect_view() + view! { <Date info=info.clone() /> } + }) + .collect_view() }} } } diff --git a/src/components/lists/multi/checkbox.rs b/src/components/lists/multi/checkbox.rs index 7515b78..252402d 100644 --- a/src/components/lists/multi/checkbox.rs +++ b/src/components/lists/multi/checkbox.rs @@ -134,25 +134,26 @@ pub fn CheckboxList( let cloned = item.clone(); move || cloned.clone() }); - let checked = Signal::derive(move || { controller.selected_list.get().contains(&item.get()) }); - let on_change = move || { - // Update the data list by checking if the value is present in it. If so, - // remove it. If not, add it. - controller.selected_list.update(|list| { - let item = item.get(); - if list.contains(&item) { - list.retain(|val| *val != item); - } else { - list.push(item); - } - }); + controller + .selected_list + .update(|list| { + let item = item.get(); + if list.contains(&item) { + list.retain(|val| *val != item); + } else { + list.push(item); + } + }); controller.dirty.set(true); }; + // Update the data list by checking if the value is present in it. If so, + // remove it. If not, add it. + view! { <Checkbox checked=checked @@ -171,7 +172,7 @@ pub fn CheckboxList( {move || { match controller.error.get() { ValidationState::Invalid(msg) => msg, - _ => "There is no error - should not see this message.".to_string() + _ => "There is no error - should not see this message.".to_string(), } }} </div> diff --git a/src/components/lists/multi/chips.rs b/src/components/lists/multi/chips.rs index d00cd8c..fdd4a22 100644 --- a/src/components/lists/multi/chips.rs +++ b/src/components/lists/multi/chips.rs @@ -45,16 +45,17 @@ pub fn Chip( id=id.clone() prop:checked=checked on:change=move |_| on_change.run(()) - disabled=disabled /> + disabled=disabled + /> <div class="rounded-full p-2.5 - cursor-pointer text-base - bg-transparent border-1 border-gray-300 - shadow-md/50 hover:shadow-md/70 - peer-checked:bg-red-700 peer-checked:border-red-700 peer-checked:text-white - peer-disabled:peer-checked:bg-gray-700 - peer-disabled:peer-checked:border-gray-700 - peer-disabled:cursor-default - transition-all duration-150"> + cursor-pointer text-base + bg-transparent border-1 border-gray-300 + shadow-md/50 hover:shadow-md/70 + peer-checked:bg-red-700 peer-checked:border-red-700 peer-checked:text-white + peer-disabled:peer-checked:bg-gray-700 + peer-disabled:peer-checked:border-gray-700 + peer-disabled:cursor-default + transition-all duration-150"> <span>{display_text.clone()}</span> </div> </label> @@ -98,47 +99,60 @@ pub fn ChipsList( <div class="m-1.5 mt-0 mb-0"> <span class="font-bold">{label}</span> <div class="flex flex-row flex-wrap gap-2 mb-1"> - {move || displayed_text.get().into_iter().zip(values.get()) - .into_iter() - .map(|(text, value)| { - let value = RwSignal::new(value); + {move || { + displayed_text + .get() + .into_iter() + .zip(values.get()) + .into_iter() + .map(|(text, value)| { + let value = RwSignal::new(value); + let checked = Signal::derive(move || { + controller.selected_list.get().contains(&value.get()) + }); + let on_change = move || { + let value = value.get(); + let list = controller.selected_list.get(); + if list.contains(&value) { + allows_multiple + .get() + .then(|| { + controller + .selected_list + .update(|list| list.retain(|v| v != &value)) + }) + .or_else(|| Some(controller.selected_list.set(Vec::new()))); + } else { + allows_multiple + .get() + .then(|| { + controller + .selected_list + .update(|list| list.push(value.clone())) + }) + .or_else(|| Some( + controller.selected_list.set(vec![value.clone()]), + )); + } + controller.dirty.set(true); + }; - let checked = Signal::derive(move || { - controller.selected_list.get().contains(&value.get()) - }); - - let on_change = move || { // Update the data list by checking if the value is present in it. If so, // remove it. If not, add it. - let value = value.get(); - let list = controller.selected_list.get(); - if list.contains(&value) { - allows_multiple.get() - .then(|| controller.selected_list.update(|list| list.retain(|v| v != &value))) - .or_else(|| Some(controller.selected_list.set(Vec::new()))); - } else { - allows_multiple.get() - .then(|| controller.selected_list.update(|list| list.push(value.clone()))) - .or_else(|| Some(controller.selected_list.set(vec![value.clone()]))); + view! { + <Chip + checked=checked + on_change=on_change + value=value + display_text=text + name=data_member + disabled=disabled + /> } - - controller.dirty.set(true); - }; - - view! { - <Chip - checked=checked - on_change=on_change - value=value - display_text=text - name=data_member - disabled=disabled - /> - } - }) - .collect_view() - } + }) + .collect_view() + }} </div> </div> <Show when=move || controller.show_errors.get()> @@ -146,7 +160,7 @@ pub fn ChipsList( {move || { match controller.error.get() { ValidationState::Invalid(msg) => msg, - _ => "There is no error - should not see this message.".to_string() + _ => "There is no error - should not see this message.".to_string(), } }} </div> diff --git a/src/components/lists/single/radio.rs b/src/components/lists/single/radio.rs index 86b4ddf..20619f9 100644 --- a/src/components/lists/single/radio.rs +++ b/src/components/lists/single/radio.rs @@ -121,19 +121,24 @@ pub fn RadioList( .into_iter() .map(|item| { let item = RwSignal::new(item); - let checked_signal = Signal::derive(move || { selected_value.get() == item.get() }); - let on_change = move || { - // on_change only runs when the radio button is checked, not when unchecked. - // Change the selected value to the new one. selected_value.set(item.get()); - error.set(validate(selected_value.get_untracked(), required.get_untracked())); + error + .set( + validate( + selected_value.get_untracked(), + required.get_untracked(), + ), + ); dirty.set(true); }; + // on_change only runs when the radio button is checked, not when unchecked. + // Change the selected value to the new one. + view! { <Radio checked=checked_signal @@ -152,7 +157,7 @@ pub fn RadioList( {move || { match error.get() { ValidationState::Invalid(msg) => msg, - _ => "There is no error - should not see this message.".to_string() + _ => "There is no error - should not see this message.".to_string(), } }} </div> diff --git a/src/components/lists/single/select.rs b/src/components/lists/single/select.rs index 3fd6ab6..b20cddc 100644 --- a/src/components/lists/single/select.rs +++ b/src/components/lists/single/select.rs @@ -93,9 +93,9 @@ pub fn Select( <span class="ml-1.5 mb-0 font-bold">{label}</span> <select class="relative flex-1 border-2 m-1.5 mt-0 p-1.5 rounded-md - transition-border duration-150 - border-red-700 bg-transparent - disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" + transition-border duration-150 + border-red-700 bg-transparent + disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" on:change:target=on_change disabled=disabled > @@ -104,15 +104,18 @@ pub fn Select( value="" disabled hidden - selected={move || raw_value.get().is_empty()} - >"Select one..."</option> + selected=move || raw_value.get().is_empty() + > + "Select one..." + </option> {move || { - value_list.iter().map( - move |value| { + value_list + .iter() + .map(move |value| { let display = value.to_owned(); - view! { <option value={display}>{display.clone()}</option> } - } - ).collect::<Vec<_>>() + view! { <option value=display>{display.clone()}</option> } + }) + .collect::<Vec<_>>() }} </select> </label> @@ -122,7 +125,7 @@ pub fn Select( {move || { match error.get() { ValidationState::Invalid(msg) => msg, - _ => "There is no error - should not see this message.".to_string() + _ => "There is no error - should not see this message.".to_string(), } }} </div> diff --git a/src/components/text_field.rs b/src/components/text_field.rs index dee1854..40e9052 100644 --- a/src/components/text_field.rs +++ b/src/components/text_field.rs @@ -144,12 +144,12 @@ pub fn OutlinedTextField( <input node_ref=input_ref class="border-2 m-1.5 p-1.5 mt-0 rounded-md bg-transparent relative flex-1 - transition-all duration-150 - border-red-700 bg-transparent - disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" - r#type={str_type} - disabled={disabled} - placeholder={placeholder} + transition-all duration-150 + border-red-700 bg-transparent + disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" + r#type=str_type + disabled=disabled + placeholder=placeholder prop:name=name prop:value=raw_value on:input=on_input @@ -160,7 +160,7 @@ pub fn OutlinedTextField( {move || { match error.get() { ValidationState::Invalid(msg) => msg, - _ => "There is no error - should not see this message.".to_string() + _ => "There is no error - should not see this message.".to_string(), } }} </div> diff --git a/src/pages/comparison_test.rs b/src/pages/comparison_test.rs index d01f03e..57afa5a 100644 --- a/src/pages/comparison_test.rs +++ b/src/pages/comparison_test.rs @@ -57,93 +57,84 @@ pub fn ComparisonTestPage() -> impl IntoView { let student_info = student_resource.get(); let comparison_info = comparison_resource.get(); let scholarship_info = scholarship_resource.get(); - match (student_info, comparison_info, scholarship_info) { - (Some(student), Some(comparisons), Some(scholarships)) => Either::Right(view! { - <Panel> - <Row> - <p>{result_msg}</p> - </Row> - <Row> - <ActionButton on:click=move |_| { - // Iterate over all scholarships, get the comparison IDs, - // get ComparisonData from the IDs, then perform the - // comparisons and return a boolean. Alternatively, return - // a list of comparisons that complete and a list that don't. - let comp_member = "requirements".to_string(); - let comparison_results = scholarships.iter().map(|scholarship| { - let list = match scholarship.data.get(&comp_member) { - Some(ValueType::List(Some(list))) => list, - _ => { - log!("Cannot find requirements, assuming none."); - return Ok(Some(scholarship)) // students will always qualify when there are no requirements - } - }; + (Some(student), Some(comparisons), Some(scholarships)) => { + Either::Right( - let mut final_result = Ok(true); - - // The list should exist, and it should contain a series of strings. - for val in list { - let comp_opt = match val { - ValueType::String(Some(comp_id)) => { - // Find the comparison - comparisons.iter().find(|comp| comp.id == *comp_id) - } - _ => None - }; - - let comparison_outcome = match comp_opt { - Some(comp) => { - // Perform the comparison - comp.compare(&student) - } - _ => Err("".to_string()) - }; - - match comparison_outcome { - Ok(true) => {}, - Ok(false) => final_result = Ok(false), - Err(e) => { - final_result = Err(e); - break; + view! { + <Panel> + <Row> + <p>{result_msg}</p> + </Row> + <Row> + <ActionButton on:click=move |_| { + let comp_member = "requirements".to_string(); + let comparison_results = scholarships + .iter() + .map(|scholarship| { + let list = match scholarship.data.get(&comp_member) { + Some(ValueType::List(Some(list))) => list, + _ => { + log!("Cannot find requirements, assuming none."); + return Ok(Some(scholarship)); + } + }; + let mut final_result = Ok(true); + for val in list { + let comp_opt = match val { + ValueType::String(Some(comp_id)) => { + comparisons.iter().find(|comp| comp.id == *comp_id) + } + _ => None, + }; + let comparison_outcome = match comp_opt { + Some(comp) => comp.compare(&student), + _ => Err("".to_string()), + }; + match comparison_outcome { + Ok(true) => {} + Ok(false) => final_result = Ok(false), + Err(e) => { + final_result = Err(e); + break; + } + } + } + match final_result { + Ok(true) => Ok(Some(scholarship)), + Ok(false) => Ok(None), + Err(e) => Err(e), + } + }) + .collect::<Vec<Result<Option<&ExpandableInfo>, String>>>(); + let mut strings = Vec::new(); + strings.push("Scholarship results: \n".to_string()); + for result in comparison_results { + match result { + Ok(Some(scholarship_info)) => { + let name_member = "name".to_string(); + let scl_name = scholarship_info + .data + .get(&name_member) + .unwrap() + .as_string() + .unwrap() + .unwrap(); + strings.push(scl_name); + } + Ok(None) => {} + Err(msg) => strings.push(msg), } } - } - - match final_result { - Ok(true) => Ok(Some(scholarship)), - Ok(false) => Ok(None), - Err(e) => Err(e) - } - }).collect::<Vec<Result<Option<&ExpandableInfo>, String>>>(); - - // Build a display string - let mut strings = Vec::new(); - strings.push("Scholarship results: \n".to_string()); - for result in comparison_results { - match result { - Ok(Some(scholarship_info)) => { - // Get the scholarship name. - let name_member = "name".to_string(); - let scl_name = scholarship_info.data.get(&name_member).unwrap() - .as_string().unwrap().unwrap(); - strings.push(scl_name); - }, - Ok(None) => {}, - Err(msg) => strings.push(msg) - } - } - - let display_text = strings.join("\n").to_string(); - - result_msg.set(display_text); - }>"Test Comparison"</ActionButton> - </Row> - </Panel> - }), - _ => Either::Left(view! { - <p>"Failed API requests."</p> - }) + let display_text = strings.join("\n").to_string(); + result_msg.set(display_text); + }>"Test Comparison"</ActionButton> + </Row> + </Panel> + }, + ) + } + _ => Either::Left(view! { <p>"Failed API requests."</p> }), } }} </Suspense> diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 7f58d4d..f303195 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -289,7 +289,7 @@ pub fn HomePage() -> impl IntoView { data_member="contact_email" data_map=form_data input_type=TextFieldType::Email( - vec!["fakeemail.org".to_string(), "gmail.com".to_string()] + vec!["fakeemail.org".to_string(), "gmail.com".to_string()], ) required=true /> @@ -369,8 +369,12 @@ pub fn HomePage() -> impl IntoView { data_map=form_data data_member="community_involvement" schema=vec![ - input!(Text, "service_name", "Activity Name:", true, "Some service activity..."), - input!(Number, "service_hours", "Total Service Hours", true, "20") + input!( + Text, "service_name", "Activity Name:", true, "Some service activity..." + ), + input!( + Number, "service_hours", "Total Service Hours", true, "20" + ), ] /> </Row> @@ -380,8 +384,7 @@ pub fn HomePage() -> impl IntoView { </Row> } }) - }} - <Row> + }} <Row> <ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton> </Row> </Suspense> diff --git a/src/pages/loaner_page.rs b/src/pages/loaner_page.rs index 07b2464..14cd5b0 100644 --- a/src/pages/loaner_page.rs +++ b/src/pages/loaner_page.rs @@ -213,11 +213,7 @@ pub fn LoanerShell() -> impl IntoView { // We'll create the page here - the sidebar contains all the buttons, while the form // container either shows forms or shows placeholder text. view! { - <Banner - title="Chromebook Loaners" - logo="/PHS_Stacked_Acronym.png" - path="/loaners" - /> + <Banner title="Chromebook Loaners" logo="/PHS_Stacked_Acronym.png" path="/loaners" /> <div class="flex m-5"> <div class="flex flex-1 flex-row gap-3"> <div id="sidebar" class="flex flex-1 flex-col gap-2"> @@ -247,16 +243,9 @@ pub fn LoanerShell() -> impl IntoView { #[component] pub fn LoanerFallback() -> impl IntoView { view! { - <div - class="flex flex-col items-center justify-center" - use:animate=pop_in_out() - > - <div class="text-3xl font-bold mb-2"> - "Chromebook Loaner Form" - </div> - <div class="text-lg"> - "Select an option from the left to begin." - </div> + <div class="flex flex-col items-center justify-center" use:animate=pop_in_out()> + <div class="text-3xl font-bold mb-2">"Chromebook Loaner Form"</div> + <div class="text-lg">"Select an option from the left to begin."</div> </div> } } @@ -431,69 +420,85 @@ pub fn LoanerReturnForm() -> impl IntoView { <dialog use:animate=pop_in_out() class="p-3 m-auto rounded-md shadow-lg/33 backdrop:backdrop-blur-xs backdrop:transition-backdrop-filter" - node_ref=dialog_ref> + node_ref=dialog_ref + > <h2 class="text-2xl font-bold">"Confirmation"</h2> <h4 class="text-xl mb-3"> - {move || chosen_student.with(|opt_student| { - if let Some(student) = opt_student.as_ref() { - format!("Are you sure want to check in {} {}?", student.first_name, student.last_name) - } else { - "There is no student selected.".to_string() - } - })} + {move || { + chosen_student + .with(|opt_student| { + if let Some(student) = opt_student.as_ref() { + format!( + "Are you sure want to check in {} {}?", + student.first_name, + student.last_name, + ) + } else { + "There is no student selected.".to_string() + } + }) + }} </h4> - <ActionButton on:click=close_dialog disabled=dialog_disabled>"Close"</ActionButton> - <ActionButton on:click=dialog_check_in disabled=dialog_disabled>"Check In"</ActionButton> + <ActionButton on:click=close_dialog disabled=dialog_disabled> + "Close" + </ActionButton> + <ActionButton on:click=dialog_check_in disabled=dialog_disabled> + "Check In" + </ActionButton> </dialog> - <div - class="flex flex-col gap-2 m-2" - use:animate=pop_in_out() - > + <div class="flex flex-col gap-2 m-2" use:animate=pop_in_out()> <div class="flex flex-col justify-center gap-2 mb-3 py-3 px-2"> <h2 class="text-2xl font-bold">"Student List"</h2> <p class="text-lg">"Please select your name from the list."</p> </div> - <Suspense fallback=move || "Loading...".into_view()> + <Suspense fallback=move || { + "Loading...".into_view() + }> {move || { match return_list_resource.get() { Some(loaner_list) => { - let list_view = loaner_list.iter().map(|student| { - let on_click = { - let student_click = student.clone(); - move |_| { - log!("Running on_click using student {:?}", student_click); - if let Some(dialog) = dialog_ref.get() { - chosen_student.set(Some(student_click.clone())); - dialog.show_modal().expect("Couldn't show modal dialog."); + let list_view = loaner_list + .iter() + .map(|student| { + let on_click = { + let student_click = student.clone(); + move |_| { + log!("Running on_click using student {:?}", student_click); + if let Some(dialog) = dialog_ref.get() { + chosen_student.set(Some(student_click.clone())); + dialog.show_modal().expect("Couldn't show modal dialog."); + } } + }; + let naive_time = chrono::NaiveDateTime::parse_from_str( + &student.date, + "%H:%M, %m/%d/%Y", + ) + .unwrap(); + let time_zone = FixedOffset::west_opt(5 * 3600).unwrap(); + let student_date = time_zone + .from_utc_datetime(&naive_time) + .format("%l:%M %p, %m/%d/%Y") + .to_string(); + log!("Found current date {:?}", student_date); + + view! { + <div + class="flex flex-row p-3 rounded-md cursor-pointer transition-shadow shadow-sm hover:shadow-lg/33" + on:click=on_click + > + <div class="flex-1"> + {student.first_name.clone()}" "{student.last_name.clone()} + </div> + <div class="flex-none">{student_date.clone()}</div> + </div> } - }; - - let naive_time = chrono::NaiveDateTime::parse_from_str(&student.date, "%H:%M, %m/%d/%Y") - .unwrap(); - let time_zone = FixedOffset::west_opt(5 * 3600).unwrap(); - let student_date = time_zone.from_utc_datetime(&naive_time) - .format("%l:%M %p, %m/%d/%Y") - .to_string(); - - log!("Found current date {:?}", student_date); - - view! { - <div class="flex flex-row p-3 rounded-md cursor-pointer transition-shadow shadow-sm hover:shadow-lg/33" on:click=on_click> - <div class="flex-1"> - {student.first_name.clone()}" "{student.last_name.clone()} - </div> - <div class="flex-none"> - {student_date.clone()} - </div> - </div> - } - }).collect_view(); - + }) + .collect_view(); Either::Left(list_view) - }, - None => Either::Right("No loaners found.".into_view()) + } + None => Either::Right("No loaners found.".into_view()), } }} </Suspense> diff --git a/src/pages/provider_contact.rs b/src/pages/provider_contact.rs index 94b8cf7..55a01b4 100644 --- a/src/pages/provider_contact.rs +++ b/src/pages/provider_contact.rs @@ -143,7 +143,7 @@ pub fn ProviderContactPage() -> impl IntoView { <div class="flex flex-row"> <div class="flex flex-col flex-1" /> <Panel> - <ValidatedForm + <ValidatedForm on_submit=on_submit title="Contact Form" description="Enter your contact information and click submit." diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index f288a7f..92db0f4 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -14,19 +14,19 @@ pub fn TestPage() -> impl IntoView { description="Edit user profile" icon="/Person_Black.png" path="/" - /> + /> <DashboardButton title="Create Scholarship" description="Navigate to Scholarship Creation Page" icon="/Create_Black.png" path="/" - /> + /> <DashboardButton title="Applicants" description="View Scholarship Applicants" icon="/Form_Black.png" path="/" - /> + /> </section> <section class="space-y-2"> <div class="rounded-lg shadow-lg/25 overflow-hidden"> @@ -35,34 +35,35 @@ pub fn TestPage() -> impl IntoView { </div> <div class="p-2"> <div class="flex flex-col p-3 gap-3"> - <DateList - dates=vec![ - DateInfo { - title: "Title".to_string(), - date: DateRange::Range("February 2nd, 2026".to_string(), "February 10th, 2026".to_string()), - description: "This is a test".to_string(), - status: DateStatus::Upcoming - }, - DateInfo { - title: "Scholarships Due".to_string(), - date: DateRange::Single("November 4th, 2025".to_string()), - description: "This is a test".to_string(), - status: DateStatus::Open - }, - DateInfo { - title: "Scholarships Due".to_string(), - date: DateRange::Single("November 4th, 2025".to_string()), - description: "This is a test".to_string(), - status: DateStatus::Deadline - }, - DateInfo { - title: "Scholarships Due".to_string(), - date: DateRange::Single("November 4th, 2025".to_string()), - description: "This is a test".to_string(), - status: DateStatus::Closed - } - ] - /> + <DateList dates=vec![ + DateInfo { + title: "Title".to_string(), + date: DateRange::Range( + "February 2nd, 2026".to_string(), + "February 10th, 2026".to_string(), + ), + description: "This is a test".to_string(), + status: DateStatus::Upcoming, + }, + DateInfo { + title: "Scholarships Due".to_string(), + date: DateRange::Single("November 4th, 2025".to_string()), + description: "This is a test".to_string(), + status: DateStatus::Open, + }, + DateInfo { + title: "Scholarships Due".to_string(), + date: DateRange::Single("November 4th, 2025".to_string()), + description: "This is a test".to_string(), + status: DateStatus::Deadline, + }, + DateInfo { + title: "Scholarships Due".to_string(), + date: DateRange::Single("November 4th, 2025".to_string()), + description: "This is a test".to_string(), + status: DateStatus::Closed, + }, + ] /> </div> </div> </div> From d42573bf65672bcb8cc8d0ed9fff593d6b2ed0c6 Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Thu, 15 Jan 2026 08:37:43 -0500 Subject: [PATCH 22/32] feat: implement auth callback page This page will allow users to redirect to their previous path when they sign in. --- src/app.rs | 9 +++----- src/pages/auth_callback.rs | 37 ++++++++++++++++++++++++++++++ src/pages/mod.rs | 4 +++- src/pages/unauthenticated_page.rs | 38 +++++++++++++++++++------------ 4 files changed, 66 insertions(+), 22 deletions(-) create mode 100644 src/pages/auth_callback.rs diff --git a/src/app.rs b/src/app.rs index 9549173..09b05f0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,10 +1,9 @@ -use crate::pages::{AboutPage, HomePage, ProviderPortal, ComparisonTestPage, ScholarshipInfoPage, TestPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm}; +use crate::pages::{AboutPage, HomePage, ProviderPortal, ComparisonTestPage, ScholarshipInfoPage, TestPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage}; use leptos::prelude::*; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_oidc::{Auth, AuthParameters, AuthSignal, Challenge}; use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path}; use crate::components::ToastList; -use leptos::logging::log; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -66,15 +65,12 @@ pub fn AppWithRoutes() -> impl IntoView { provide_meta_context(); let current_origin = use_origin(); - log!("Current origin: {}", current_origin); // Authentication setup let parameters = AuthParameters { issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM".into(), client_id: "10jr2h3vtpu9n7gj46pvg5qo2q".into(), - redirect_uri: format!("{}/", use_origin()), - // redirect_uri: Url::parse(format!("{}", current_origin).as_str()).unwrap().to_string(), - // redirect_uri: Url::parse("http://localhost:3000").unwrap().to_string(), + redirect_uri: format!("{}/auth/callback", current_origin), post_logout_redirect_uri: current_origin, scope: Some("openid%20profile%20email".into()), audience: None, @@ -103,6 +99,7 @@ pub fn AppWithRoutes() -> impl IntoView { // TODO Create a 404 page <Routes fallback=|| "Page not found.".into_view()> <Route path=path!("") view=HomePage /> + <Route path=path!("/auth/callback") view=AuthCallbackPage /> <Route path=path!("/about") view=AboutPage /> <Route path=path!("/test_page") view=TestPage /> <Route path=path!("/comparison") view=ComparisonTestPage /> diff --git a/src/pages/auth_callback.rs b/src/pages/auth_callback.rs new file mode 100644 index 0000000..4b2dbe3 --- /dev/null +++ b/src/pages/auth_callback.rs @@ -0,0 +1,37 @@ +use leptos::prelude::*; +use leptos_oidc::AuthSignal; +use leptos_router::hooks::use_navigate; + +#[component] +pub fn AuthCallbackPage() -> impl IntoView { + // This should wait for authentication, read the values in session storage, and then redirect + // to the correct page. The session storage should contain a path, which we'll need to check. + + let auth = expect_context::<AuthSignal>(); + let navigate = use_navigate(); + + Effect::new(move || { + // Wait for auth to load, then read information. + if let Some(_) = auth.get().authenticated() { + // Read the sessionStorage to get the last navigated path. + let storage_opt = window().session_storage().ok().flatten(); + let path = storage_opt + .and_then(|storage| storage.get_item("post_login_path").ok().flatten()) + .unwrap_or_else(|| "/".to_string()); + + // Clear storage in case we need to reauth again + if let Some(storage) = window().session_storage().ok().flatten() { + let _ = storage.remove_item("post_login_path"); + } + + // Redirect to the stored path + navigate(&path, Default::default()); + } + }); + + view! { + <div class="flex flex-col w-full mx-auto items-center justify-center"> + <p>"Completing the login process..."</p> + </div> + } +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 4da8c68..1b363d8 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -9,8 +9,10 @@ mod test_page; mod unauthenticated_page; mod utils; mod provider_contact; +mod auth_callback; pub use self::{ about_page::*, comparison_test::*, home_page::*, loaner_page::*, provider_portal::*, - test_page::*, unauthenticated_page::*, scholarship_info::*, provider_contact::* + test_page::*, unauthenticated_page::*, scholarship_info::*, provider_contact::*, + auth_callback::*, }; diff --git a/src/pages/unauthenticated_page.rs b/src/pages/unauthenticated_page.rs index 7de72e3..e2d6857 100644 --- a/src/pages/unauthenticated_page.rs +++ b/src/pages/unauthenticated_page.rs @@ -2,35 +2,43 @@ use crate::components::ActionButton; use leptos::logging::log; use leptos::prelude::*; use leptos_oidc::AuthSignal; +use leptos_router::hooks::{use_navigate, use_location}; #[component] pub fn UnauthenticatedPage() -> impl IntoView { let auth = use_context::<AuthSignal>().expect("AuthSignal not present in LoginLink"); - let login_url = move || { + let login_url = Memo::new(move |_| { auth.with(|auth| { auth.unauthenticated() .map(|unauthenticated| unauthenticated.login_url()) }) + }); + + Effect::new(move || log!("Generated login url: {:?}", login_url.get())); + + let navigate = use_navigate(); + let location = use_location(); + + let on_click = move |_| { + let current_path = location.pathname.get(); + + let storage_opt = window().session_storage().ok().flatten(); + if let Some(session_storage) = storage_opt { + let _ = session_storage.set_item("post_login_path", ¤t_path); + } + if let Some(url) = login_url.get() { + navigate(&url, Default::default()) + } }; - Effect::new(move || log!("Generated login url: {:?}", login_url())); - - let navigate = leptos_router::hooks::use_navigate(); - - view! ( + view! { <div class="flex h-screen justify-center items-center pb-50"> <div class="bg-white p-8 border rounded-lg shadow-lg"> - <h3> "You're not signed in. Please sign in with your Region 15 email." </h3> + <h3>"You're not signed in. Please sign in with your Region 15 email."</h3> <div class="flex pt-4 justify-center items-center"> - <ActionButton on:click=move |_| { - if let Some(url) = login_url() { - navigate(&url, Default::default()) - } - }> - "Sign in" - </ActionButton> + <ActionButton on:click=on_click>"Sign in"</ActionButton> </div> </div> </div> - ) + } } From 09de9fcf9114a4f6aff0687b9d9bdccc18e9da28 Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Thu, 15 Jan 2026 09:48:21 -0500 Subject: [PATCH 23/32] feat: implement provider and student specific login contexts --- src/app.rs | 89 +++++++++++++------------------- src/components/login/mod.rs | 5 ++ src/components/login/provider.rs | 30 +++++++++++ src/components/login/setup.rs | 24 +++++++++ src/components/login/student.rs | 30 +++++++++++ src/components/mod.rs | 1 + src/lib.rs | 1 + src/pages/api/comparisons.rs | 4 +- src/pages/api/scholarships.rs | 3 +- src/pages/comparison_test.rs | 2 +- src/pages/home_page.rs | 2 +- src/pages/mod.rs | 1 - src/pages/provider_contact.rs | 11 ++-- src/pages/scholarship_info.rs | 2 +- src/pages/utils.rs | 42 --------------- src/utils/common.rs | 33 ++++++++++++ src/utils/mod.rs | 5 ++ src/utils/server.rs | 17 ++++++ 18 files changed, 193 insertions(+), 109 deletions(-) create mode 100644 src/components/login/mod.rs create mode 100644 src/components/login/provider.rs create mode 100644 src/components/login/setup.rs create mode 100644 src/components/login/student.rs delete mode 100644 src/pages/utils.rs create mode 100644 src/utils/common.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/server.rs diff --git a/src/app.rs b/src/app.rs index 09b05f0..bb3064d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,10 @@ use crate::pages::{AboutPage, HomePage, ProviderPortal, ComparisonTestPage, ScholarshipInfoPage, TestPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage}; use leptos::prelude::*; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; -use leptos_oidc::{Auth, AuthParameters, AuthSignal, Challenge}; -use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path}; +use leptos_oidc::AuthSignal; +use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path, MatchNestedRoutes}; +use leptos_router::any_nested_route::IntoAnyNestedRoute; +use crate::components::login::ProviderLoginContext; use crate::components::ToastList; pub fn shell(options: LeptosOptions) -> impl IntoView { @@ -43,54 +45,15 @@ pub fn App() -> impl IntoView { } } -fn use_origin() -> String { - #[cfg(target_arch = "wasm32")] { - use leptos::web_sys; - // Just get the current URL origin. - web_sys::window() - .unwrap() - .location() - .origin() - .unwrap_or("http://localhost:3000/".to_string()) - } - - #[cfg(not(target_arch = "wasm32"))] { - // Read the expected origin out of an environment variable. - std::env::var("LP_SITE_ORIGIN").unwrap_or("http://localhost:3000/".to_string()) - } -} - #[component] pub fn AppWithRoutes() -> impl IntoView { provide_meta_context(); - let current_origin = use_origin(); - - // Authentication setup - let parameters = AuthParameters { - issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM".into(), - client_id: "10jr2h3vtpu9n7gj46pvg5qo2q".into(), - redirect_uri: format!("{}/auth/callback", current_origin), - post_logout_redirect_uri: current_origin, - scope: Some("openid%20profile%20email".into()), - audience: None, - challenge: Challenge::None, - }; - - let auth = Auth::signal(); - provide_context(auth); // allows use of this signal in lower areas of the tree without - // explicitly passing it through the html tree - - let _ = Auth::init(parameters); - view! { // injects a stylesheet into the document <head> // id=leptos means cargo-leptos will hot-reload this stylesheet <Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css" /> - // <AuthLoading><p>"Authentication is loading"</p></AuthLoading> - // <AuthErrorContext><AuthErrorView/></AuthErrorContext> - // sets the document title <Title text="R15 Scholarship App DEV" /> @@ -99,27 +62,47 @@ pub fn AppWithRoutes() -> impl IntoView { // TODO Create a 404 page <Routes fallback=|| "Page not found.".into_view()> <Route path=path!("") view=HomePage /> - <Route path=path!("/auth/callback") view=AuthCallbackPage /> <Route path=path!("/about") view=AboutPage /> <Route path=path!("/test_page") view=TestPage /> <Route path=path!("/comparison") view=ComparisonTestPage /> - <Route path=path!("/providers") view=ProviderPortal /> - <Route path=path!("/providers/profile") view=ProviderContactPage /> - <ParentRoute path=path!("/providers/scholarships") view=ScholarshipInfoPage> - <Route path=path!(":id") view=ScholarshipInfoPage /> - <Route path=path!("") view=ScholarshipInfoPage /> - </ParentRoute> - <ParentRoute path=path!("loaners") view=LoanerShell> - <Route path=path!("") view=LoanerFallback /> - <Route path=path!("borrowing") view=LoanerBorrowForm /> - <Route path=path!("returning") view=LoanerReturnForm /> - </ParentRoute> + <ProviderRoutes /> + <LoanerRoutes /> </Routes> </ToastList> </main> } } +#[component(transparent)] +fn ProviderRoutes() -> impl MatchNestedRoutes + Clone { + view! { + <ParentRoute path=path!("/providers") view=ProviderLoginContext> + <Route path=path!("/callback") view=AuthCallbackPage /> + <Route path=path!("") view=ProviderPortal /> + <Route path=path!("/profile") view=ProviderContactPage /> + <ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage> + <Route path=path!(":id") view=ScholarshipInfoPage /> + <Route path=path!("") view=ScholarshipInfoPage /> + </ParentRoute> + </ParentRoute> + } + .into_inner() + .into_any_nested_route() +} + +#[component(transparent)] +fn LoanerRoutes() -> impl MatchNestedRoutes + Clone { + view! { + <ParentRoute path=path!("loaners") view=LoanerShell> + <Route path=path!("") view=LoanerFallback /> + <Route path=path!("borrowing") view=LoanerBorrowForm /> + <Route path=path!("returning") view=LoanerReturnForm /> + </ParentRoute> + } + .into_inner() + .into_any_nested_route() +} + #[component] pub fn AuthErrorView() -> impl IntoView { let auth = use_context::<AuthSignal>().expect("AuthError: RwSignal<AuthStore> was not found"); diff --git a/src/components/login/mod.rs b/src/components/login/mod.rs new file mode 100644 index 0000000..fae50eb --- /dev/null +++ b/src/components/login/mod.rs @@ -0,0 +1,5 @@ +mod provider; +pub use provider::*; +mod student; +pub use student::*; +pub(crate) mod setup; \ No newline at end of file diff --git a/src/components/login/provider.rs b/src/components/login/provider.rs new file mode 100644 index 0000000..ce1fb6d --- /dev/null +++ b/src/components/login/provider.rs @@ -0,0 +1,30 @@ +use leptos::prelude::*; +use leptos_router::components::Outlet; +use super::setup::provide_auth_context; + +///# Provider Login Context Component +/// +/// This component handles providing context to the provider login information. +/// It creates the corresponding [`AuthSignal`], specific to the student login page and redirects. +/// +/// Example usage: +/// ``` +/// view! { +/// <ProviderLoginContext> +/// // The rest of the student application. Don't forget AuthLoaded and +/// // Authenticated components! +/// </ProviderLoginContext> +/// } +#[component] +pub fn ProviderLoginContext() -> impl IntoView { + provide_auth_context( + crate::utils::use_origin(), + "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM", + "56c2bqvl021rv8d5mq36blt7jv", + "/providers/callback" + ); + + view! { + <Outlet /> + } +} diff --git a/src/components/login/setup.rs b/src/components/login/setup.rs new file mode 100644 index 0000000..a5ebae6 --- /dev/null +++ b/src/components/login/setup.rs @@ -0,0 +1,24 @@ +use leptos::prelude::*; +use leptos_oidc::{Auth, AuthParameters, Challenge}; + +pub fn provide_auth_context( + origin: String, + issuer: impl Into<String>, + client_id: impl Into<String>, + redirect_path: impl Into<String>, +) { + let params = AuthParameters { + issuer: issuer.into(), + client_id: client_id.into(), + redirect_uri: format!("{}{}", origin, redirect_path.into()), + post_logout_redirect_uri: origin, + scope: Some("openid%20profile%20email".into()), + audience: None, + challenge: Challenge::None, + }; + + let auth = Auth::signal(); + provide_context(auth); + + let _ = Auth::init(params); +} diff --git a/src/components/login/student.rs b/src/components/login/student.rs new file mode 100644 index 0000000..f3b89df --- /dev/null +++ b/src/components/login/student.rs @@ -0,0 +1,30 @@ +use leptos::prelude::*; +use leptos_router::components::Outlet; +use super::setup::provide_auth_context; + +///# Student Login Context Component +/// +/// This component handles providing context to the student login information. +/// It creates the corresponding [`AuthSignal`], specific to the student login page and redirects. +/// +/// Example usage: +/// ``` +/// view! { +/// <StudentLoginContext> +/// // The rest of the student application. Don't forget AuthLoaded and +/// // Authenticated components! +/// </StudentLoginContext> +/// } +#[component] +pub fn StudentLoginContext() -> impl IntoView { + provide_auth_context( + crate::utils::use_origin(), + "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM", + "10jr2h3vtpu9n7gj46pvg5qo2q", + "/students/callback" + ); + + view! { + <Outlet /> + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index ade0f49..27d6b69 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -12,6 +12,7 @@ mod validated_form; mod lists; mod toasts; mod header; +pub mod login; pub use self::{ action_button::*, banner::*, dashboard_button::*, lists::*, diff --git a/src/lib.rs b/src/lib.rs index 9958e9f..7ed69e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; mod common; mod components; mod pages; +mod utils; #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] diff --git a/src/pages/api/comparisons.rs b/src/pages/api/comparisons.rs index b60acef..bd00cd2 100644 --- a/src/pages/api/comparisons.rs +++ b/src/pages/api/comparisons.rs @@ -1,6 +1,5 @@ -use std::collections::HashMap; #[cfg(feature = "ssr")] -use crate::pages::utils::server_utils::create_dynamo_client; +use crate::utils::server::create_dynamo_client; #[cfg(feature = "ssr")] use aws_sdk_dynamodb::error::ProvideErrorMetadata; @@ -12,6 +11,7 @@ use crate::common::{ ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison, TextComparison, TextListComparison, ValueType, }; +use std::collections::HashMap; use leptos::prelude::ServerFnError; use leptos::server; diff --git a/src/pages/api/scholarships.rs b/src/pages/api/scholarships.rs index aaf5d47..d4071d0 100644 --- a/src/pages/api/scholarships.rs +++ b/src/pages/api/scholarships.rs @@ -8,7 +8,7 @@ use leptos::leptos_dom::log; #[cfg(feature = "ssr")] use uuid::Uuid; #[cfg(feature = "ssr")] -use crate::pages::utils::server_utils::create_dynamo_client; +use crate::utils::server::{create_dynamo_client, into_attr_map}; #[cfg(feature = "ssr")] use crate::common::ValueType; @@ -51,7 +51,6 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn #[server(CreateScholarshipInfo, endpoint = "/scholarship/info/create")] pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> { - use crate::pages::utils::server_utils::create_dynamo_client; use aws_sdk_dynamodb::error::ProvideErrorMetadata; let client = create_dynamo_client().await; diff --git a/src/pages/comparison_test.rs b/src/pages/comparison_test.rs index 57afa5a..50dd46f 100644 --- a/src/pages/comparison_test.rs +++ b/src/pages/comparison_test.rs @@ -3,12 +3,12 @@ use super::api::{get_all_scholarship_info, get_comparison_info}; use super::home_page::get_submission; use crate::common::{ComparisonData, ExpandableInfo, ValueType}; use crate::components::{ActionButton, Loading, Panel, Row}; -use crate::pages::utils::get_user_claims; use leptos::either::Either; use leptos::leptos_dom::logging::console_log; use leptos::logging::log; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; +use crate::utils::get_user_claims; #[component] pub fn ComparisonTestPage() -> impl IntoView { diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index f303195..4e109a8 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -10,7 +10,6 @@ use std::io::Write; use crate::common::{ComparisonData, ExpandableInfo}; use crate::pages::UnauthenticatedPage; -use crate::pages::utils::get_user_claims; use crate::components::{ ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm @@ -26,6 +25,7 @@ use leptos::web_sys::HtmlAnchorElement; use leptos::wasm_bindgen::JsCast; use std::collections::HashMap; use crate::pages::api::get_comparisons_categorized; +use crate::utils::get_user_claims; /// # Get Student Info /// Gets a student's information given their `subject`. diff --git a/src/pages/mod.rs b/src/pages/mod.rs index 1b363d8..6b76373 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -7,7 +7,6 @@ mod provider_portal; mod scholarship_info; mod test_page; mod unauthenticated_page; -mod utils; mod provider_contact; mod auth_callback; diff --git a/src/pages/provider_contact.rs b/src/pages/provider_contact.rs index 55a01b4..ea03899 100644 --- a/src/pages/provider_contact.rs +++ b/src/pages/provider_contact.rs @@ -1,14 +1,14 @@ -use std::collections::HashMap; +#[cfg(feature = "ssr")] +use crate::utils::server::{create_dynamo_client, into_attr_map}; + +use std::collections::HashMap; use leptos::logging::log; use leptos::prelude::*; use leptos_oidc::{AuthLoaded, Authenticated}; use crate::common::ValueType; use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm}; use crate::pages::UnauthenticatedPage; -use crate::pages::utils::get_user_claims; - -#[cfg(feature = "ssr")] -use crate::pages::utils::server_utils::create_dynamo_client; +use crate::utils::get_user_claims; #[cfg(feature = "ssr")] static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts"; @@ -52,7 +52,6 @@ async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, #[server] async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> { use aws_sdk_dynamodb::error::ProvideErrorMetadata; - use crate::pages::utils::server_utils::into_attr_map; let client = create_dynamo_client().await; diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index df41afd..53bca79 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -8,7 +8,7 @@ use leptos_router::hooks::{use_navigate, use_params}; use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType}; use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm}; use super::UnauthenticatedPage; -use crate::pages::utils::get_user_claims; +use crate::utils::get_user_claims; use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized, CreateTestComparisons}; diff --git a/src/pages/utils.rs b/src/pages/utils.rs deleted file mode 100644 index cc26488..0000000 --- a/src/pages/utils.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::common::UserClaims; -use leptos::prelude::{Signal, With, use_context}; -use leptos_oidc::{Algorithm, AuthSignal, TokenData}; - -/// # Server Utilities -/// -/// These utility functions can only be used from the server. They are created to use server-side -/// dependencies like the AWS SDK. Use of these functions will crash the front end, as these -/// dependencies cannot be compiled for WASM (nor should they be). -#[cfg(feature = "ssr")] -pub mod server_utils { - use std::collections::HashMap; - use aws_config::SdkConfig; - use crate::common::ValueType; - - /// Creates an [`SdkConfig`] struct for use with AWS SDK structs. - pub async fn create_aws_config() -> SdkConfig { - aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await - } - - /// Creates a DynamoDB client. - pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client { - aws_sdk_dynamodb::Client::new(&create_aws_config().await) - } - - pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> { - map.into_iter().map(|(k, v)| (k, v.into())).collect() - } -} - -/// Gets the current user claims. This function should only be used in an area that has -/// access to an AuthSignal, or it will result in a total failure. -pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> { - let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal."); - Signal::derive(move || { - auth.with(|auth| { - auth.authenticated().and_then(|data| { - data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"]) - }) - }) - }) -} diff --git a/src/utils/common.rs b/src/utils/common.rs new file mode 100644 index 0000000..a80584d --- /dev/null +++ b/src/utils/common.rs @@ -0,0 +1,33 @@ +use leptos::prelude::*; +use leptos_oidc::{Algorithm, AuthSignal, TokenData}; +use crate::common::UserClaims; + +pub fn use_origin() -> String { + #[cfg(target_arch = "wasm32")] { + use leptos::web_sys; + // Just get the current URL origin. + web_sys::window() + .unwrap() + .location() + .origin() + .unwrap_or("http://localhost:3000/".to_string()) + } + + #[cfg(not(target_arch = "wasm32"))] { + // Read the expected origin out of an environment variable. + std::env::var("LP_SITE_ORIGIN").unwrap_or("http://localhost:3000/".to_string()) + } +} + +/// Gets the current user claims. This function should only be used in an area that has +/// access to an AuthSignal, or it will result in a total failure. +pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> { + let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal."); + Signal::derive(move || { + auth.with(|auth| { + auth.authenticated().and_then(|data| { + data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"]) + }) + }) + }) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..93fe774 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "ssr")] +pub mod server; + +mod common; +pub use common::*; \ No newline at end of file diff --git a/src/utils/server.rs b/src/utils/server.rs new file mode 100644 index 0000000..e6c5c9b --- /dev/null +++ b/src/utils/server.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; +use aws_config::SdkConfig; +use crate::common::ValueType; + +/// Creates an [`SdkConfig`] struct for use with AWS SDK structs. +pub async fn create_aws_config() -> SdkConfig { + aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await +} + +/// Creates a DynamoDB client. +pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client { + aws_sdk_dynamodb::Client::new(&create_aws_config().await) +} + +pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> { + map.into_iter().map(|(k, v)| (k, v.into())).collect() +} \ No newline at end of file From 67d0f766f8d3f53a54871beab422ea0edcefdbca Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Thu, 15 Jan 2026 12:09:48 -0500 Subject: [PATCH 24/32] feat: implement automatic date status determination --- src/common/date_info.rs | 9 +++---- src/components/date.rs | 50 +++++++++++++++++++++++++++++++++++---- src/pages/test_page.rs | 52 +++++++++++++++++++++++++++++------------ 3 files changed, 87 insertions(+), 24 deletions(-) diff --git a/src/common/date_info.rs b/src/common/date_info.rs index 0cc39bb..fbd508a 100644 --- a/src/common/date_info.rs +++ b/src/common/date_info.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, FixedOffset}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -8,17 +9,17 @@ pub struct DateInfo { pub(crate) date: DateRange, #[serde(rename = "desc")] pub(crate) description: String, - #[serde(rename = "stat")] - pub(crate) status: DateStatus, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DateRange { - Single(String), - Range(String, String), + Single(DateTime<FixedOffset>), + Range(DateTime<FixedOffset>, DateTime<FixedOffset>), } + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DateStatus { + Blank, Upcoming, Open, Deadline, diff --git a/src/components/date.rs b/src/components/date.rs index 103c8a9..337e88e 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -1,18 +1,57 @@ +use chrono::Local; use crate::common::{DateInfo, DateRange, DateStatus}; use leptos::prelude::*; fn render_dates(dates: DateRange) -> impl IntoView { let date_text = match dates { - DateRange::Single(date) => date, + DateRange::Single(date) => format!("{}", date.format("%A %B %d, %I:%M%p")), DateRange::Range(start_date, end_date) => - format!("{} - {}", start_date, end_date), + format!("{} - {}", + start_date.format("%D %I:%M%p"), + end_date.format("%D %I:%M%p") + ), }; view! { <span>{move || date_text.clone()}</span> } } -fn render_status(status: DateStatus) -> impl IntoView { - let status_text = match status { +fn get_date_status(date: &DateRange) -> DateStatus { + // We need a threshold - maybe within 5 days, the status is deadline, + // further away is open, upcoming is when the range has not started yet, + // and closed is past the date. + + match date { + DateRange::Single(_) => DateStatus::Blank, + DateRange::Range(start_date, end_date) => { + let local = Local::now(); + // Check the amount of time from now to the start date. Values that are negative + // indicate that the start date has already passed. + if dbg!(start_date.signed_duration_since(local).num_days()) > 0 { + DateStatus::Upcoming + } else { + // Check the amount of time from now to the end date. The value will be positive if + // the end date is in the future. We will be specific to the minute. + let duration_until = dbg!(end_date).signed_duration_since(local); + let num_days = dbg!(duration_until.num_days()); + let num_mins = dbg!(duration_until.num_minutes()); + + if num_mins <= 0 { + DateStatus::Closed + } else if num_days <= 5 { + DateStatus::Deadline + } else { + DateStatus::Open + } + } + } + } +} + +fn render_status(date: DateRange) -> impl IntoView { + let status = get_date_status(&date); + + let status_text = match dbg!(status.clone()) { + DateStatus::Blank => "", DateStatus::Upcoming => "Upcoming", DateStatus::Open => "Open", DateStatus::Deadline => "Deadline", @@ -20,6 +59,7 @@ fn render_status(status: DateStatus) -> impl IntoView { }; let background_class = match status { + DateStatus::Blank => "hidden border-none", DateStatus::Upcoming => "bg-yellow-100 text-yellow-800 border-yellow-100", DateStatus::Open => "bg-green-100 text-green-800 border-green-100", DateStatus::Deadline | DateStatus::Closed => "bg-red-100 text-red-800 border-red-100", @@ -72,7 +112,7 @@ fn Date( </h3> <p class="text-sm text-gray-600">{info.get().description}</p> </div> - <div class="flex-shrink-0 ml-2">{move || render_status(info.get().status)}</div> + <div class="flex-shrink-0 ml-2">{move || render_status(info.get().date)}</div> </div> </div> </div> diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index 92db0f4..546c41e 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -1,4 +1,5 @@ -use crate::common::{DateInfo, DateRange, DateStatus}; +use chrono::DateTime; +use crate::common::{DateInfo, DateRange}; use crate::components::{DashboardButton, DateList}; use leptos::prelude::*; @@ -37,32 +38,53 @@ pub fn TestPage() -> impl IntoView { <div class="flex flex-col p-3 gap-3"> <DateList dates=vec![ DateInfo { - title: "Title".to_string(), + title: "Open Example".to_string(), date: DateRange::Range( - "February 2nd, 2026".to_string(), - "February 10th, 2026".to_string(), + DateTime::parse_from_rfc3339("2026-01-15T07:15:00-05:00") + .unwrap_or_default(), + DateTime::parse_from_rfc3339("2026-01-30T14:05:00-05:00") + .unwrap_or_default() ), description: "This is a test".to_string(), - status: DateStatus::Upcoming, }, DateInfo { - title: "Scholarships Due".to_string(), - date: DateRange::Single("November 4th, 2025".to_string()), - description: "This is a test".to_string(), - status: DateStatus::Open, + title: "Closed Example".to_string(), + date: DateRange::Range( + DateTime::parse_from_rfc3339("2026-01-01T07:15:00-05:00") + .unwrap_or_default(), + DateTime::parse_from_rfc3339("2026-01-14T14:05:00-05:00") + .unwrap_or_default() + ), + description: "".to_string(), }, DateInfo { - title: "Scholarships Due".to_string(), - date: DateRange::Single("November 4th, 2025".to_string()), + title: "Deadline Example".to_string(), + date: DateRange::Range( + DateTime::parse_from_rfc3339("2026-01-15T07:15:00-05:00") + .unwrap_or_default(), + DateTime::parse_from_rfc3339("2026-01-20T14:05:00-05:00") + .unwrap_or_default() + ), description: "This is a test".to_string(), - status: DateStatus::Deadline, }, DateInfo { - title: "Scholarships Due".to_string(), - date: DateRange::Single("November 4th, 2025".to_string()), + title: "Upcoming Example".to_string(), + date: DateRange::Range( + DateTime::parse_from_rfc3339("2026-01-30T07:15:00-05:00") + .unwrap_or_default(), + DateTime::parse_from_rfc3339("2026-02-20T14:05:00-05:00") + .unwrap_or_default() + ), description: "This is a test".to_string(), - status: DateStatus::Closed, }, + DateInfo { + title: "Single Date (Blank)".to_string(), + date: DateRange::Single( + DateTime::parse_from_rfc3339("2026-01-15T14:05:00-05:00") + .unwrap_or_default() + ), + description: "".to_string(), + } ] /> </div> </div> From 5ea7461a4d4eea2f0332cd1e8446b1e7cd69c124 Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Thu, 15 Jan 2026 12:13:24 -0500 Subject: [PATCH 25/32] chore: add new appearance for closed ranges --- src/components/date.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/date.rs b/src/components/date.rs index 337e88e..541aed2 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -62,7 +62,8 @@ fn render_status(date: DateRange) -> impl IntoView { DateStatus::Blank => "hidden border-none", DateStatus::Upcoming => "bg-yellow-100 text-yellow-800 border-yellow-100", DateStatus::Open => "bg-green-100 text-green-800 border-green-100", - DateStatus::Deadline | DateStatus::Closed => "bg-red-100 text-red-800 border-red-100", + DateStatus::Deadline => "bg-red-100 text-red-800 border-red-100", + DateStatus::Closed => "bg-red-700 text-white border-red-700" }; let classes = From f15d1516945c886024c02043304880e094c82789 Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Thu, 15 Jan 2026 13:57:12 -0500 Subject: [PATCH 26/32] feat: create date api and dynamo context The DynamoClient context should be used across the entire application - there are some significant performance improvements when under real load. --- src/lib.rs | 2 +- src/main.rs | 23 ++++++++++--- src/pages/api/dates.rs | 75 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/pages/api/dates.rs diff --git a/src/lib.rs b/src/lib.rs index 7ed69e6..b512299 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ pub mod app; mod common; mod components; mod pages; -mod utils; +pub mod utils; #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] diff --git a/src/main.rs b/src/main.rs index 1d9b6f6..2900ec5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,12 @@ async fn main() { use leptos::prelude::*; use leptos_axum::{LeptosRoutes, generate_route_list}; use scholarships_rs::app::*; + use scholarships_rs::utils::server::create_dynamo_client; + use std::sync::Arc; + let dynamo_client = create_dynamo_client().await; + let shared_client = Arc::new(dynamo_client); + let conf = get_configuration(None).unwrap(); let addr = conf.leptos_options.site_addr; let leptos_options = conf.leptos_options; @@ -14,10 +19,20 @@ async fn main() { let routes = generate_route_list(App); let app = Router::new() - .leptos_routes(&leptos_options, routes, { - let leptos_options = leptos_options.clone(); - move || shell(leptos_options.clone()) - }) + .leptos_routes_with_context( + &leptos_options, + routes, + { + let shared_client = Arc::clone(&shared_client); + move || { + provide_context(Arc::clone(&shared_client)); + } + }, + { + let leptos_options = leptos_options.clone(); + move || shell(leptos_options.clone()) + } + ) .fallback(leptos_axum::file_and_error_handler(shell)) .with_state(leptos_options); diff --git a/src/pages/api/dates.rs b/src/pages/api/dates.rs new file mode 100644 index 0000000..12d5cdf --- /dev/null +++ b/src/pages/api/dates.rs @@ -0,0 +1,75 @@ +use std::collections::HashMap; +use leptos::logging::log; +use leptos::prelude::*; +use crate::common::DateInfo; + +#[cfg(feature = "ssr")] +static DATES_TABLE: &str = "leptos-dates"; + +#[cfg(feature = "ssr")] +use aws_sdk_dynamodb::{ + types::{WriteRequest, PutRequest}, + Client as DynamoClient +}; + +#[cfg(feature = "ssr")] +use std::sync::Arc; + +#[server] +pub async fn create_dates(dates: Vec<DateInfo>) -> Result<(), ServerFnError> { + // Serialize and put dates into the database. + let client = expect_context::<Arc<DynamoClient>>(); + + log!("Creating dates: {:?}", dates); + + let requests = dates + .into_iter() + .map(|date| { + let item = serde_dynamo::to_item(&date) + .unwrap_or_default(); + + let put_request = PutRequest::builder() + .set_item(Some(item)) + .build() + .expect("Couldn't build request, item is not present"); + + WriteRequest::builder().put_request(put_request).build() + }) + .collect::<Vec<WriteRequest>>(); + + let mut request_map = HashMap::new(); + request_map.insert(DATES_TABLE.to_string(), requests); + + client + .batch_write_item() + .set_request_items(Some(request_map)) + .send() + .await + .map(|_| ()) + .map_err(ServerFnError::from) +} + +#[server] +pub async fn get_important_dates() -> Result<Vec<DateInfo>, ServerFnError> { + let client = expect_context::<Arc<DynamoClient>>(); + + client + .scan() + .table_name(DATES_TABLE) + .send() + .await + .map(|output| { + if let Some(items) = output.items { + items + .into_iter() + .filter_map(|item| { + serde_dynamo::from_item::<_, DateInfo>(item) + .ok() + }) + .collect::<Vec<DateInfo>>() + } else { + Vec::new() + } + }) + .map_err(ServerFnError::from) +} From 7d7c96e6420701db60372bd46f36f880bfa18459 Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Thu, 15 Jan 2026 13:57:59 -0500 Subject: [PATCH 27/32] feat: sort dates list before displaying the list --- src/common/date_info.rs | 60 +++++++++++++---- src/components/date.rs | 38 +---------- src/pages/api/mod.rs | 5 +- src/pages/test_page.rs | 146 ++++++++++++++++++++++++++-------------- 4 files changed, 150 insertions(+), 99 deletions(-) diff --git a/src/common/date_info.rs b/src/common/date_info.rs index fbd508a..269c300 100644 --- a/src/common/date_info.rs +++ b/src/common/date_info.rs @@ -1,14 +1,20 @@ -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, Local}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DateInfo { - #[serde()] - pub(crate) title: String, - #[serde(rename = "date")] - pub(crate) date: DateRange, - #[serde(rename = "desc")] - pub(crate) description: String, + pub id: String, + pub title: String, + pub date: DateRange, + pub description: String, +} + +impl DateInfo { + pub fn get_start_date(&self) -> DateTime<FixedOffset> { + match self.date { + DateRange::Single(date) | DateRange::Range(date, _) => date, + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,11 +23,41 @@ pub enum DateRange { Range(DateTime<FixedOffset>, DateTime<FixedOffset>), } -#[derive(Debug, Clone, Serialize, Deserialize)] +impl DateRange { + pub fn get_status(&self) -> DateStatus { + match self { + DateRange::Single(_) => DateStatus::Blank, + DateRange::Range(start_date, end_date) => { + let local = Local::now(); + // Check the amount of time from now to the start date. Values that are negative + // indicate that the start date has already passed. + if dbg!(start_date.signed_duration_since(local).num_days()) > 0 { + DateStatus::Upcoming + } else { + // Check the amount of time from now to the end date. The value will be positive if + // the end date is in the future. We will be specific to the minute. + let duration_until = dbg!(end_date).signed_duration_since(local); + let num_days = dbg!(duration_until.num_days()); + let num_mins = dbg!(duration_until.num_minutes()); + + if num_mins <= 0 { + DateStatus::Closed + } else if num_days <= 5 { + DateStatus::Deadline + } else { + DateStatus::Open + } + } + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)] pub enum DateStatus { + Closed, + Deadline, + Open, + Upcoming, Blank, - Upcoming, - Open, - Deadline, - Closed, } diff --git a/src/components/date.rs b/src/components/date.rs index 541aed2..88c66f8 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -1,4 +1,3 @@ -use chrono::Local; use crate::common::{DateInfo, DateRange, DateStatus}; use leptos::prelude::*; @@ -15,40 +14,8 @@ fn render_dates(dates: DateRange) -> impl IntoView { view! { <span>{move || date_text.clone()}</span> } } -fn get_date_status(date: &DateRange) -> DateStatus { - // We need a threshold - maybe within 5 days, the status is deadline, - // further away is open, upcoming is when the range has not started yet, - // and closed is past the date. - - match date { - DateRange::Single(_) => DateStatus::Blank, - DateRange::Range(start_date, end_date) => { - let local = Local::now(); - // Check the amount of time from now to the start date. Values that are negative - // indicate that the start date has already passed. - if dbg!(start_date.signed_duration_since(local).num_days()) > 0 { - DateStatus::Upcoming - } else { - // Check the amount of time from now to the end date. The value will be positive if - // the end date is in the future. We will be specific to the minute. - let duration_until = dbg!(end_date).signed_duration_since(local); - let num_days = dbg!(duration_until.num_days()); - let num_mins = dbg!(duration_until.num_minutes()); - - if num_mins <= 0 { - DateStatus::Closed - } else if num_days <= 5 { - DateStatus::Deadline - } else { - DateStatus::Open - } - } - } - } -} - fn render_status(date: DateRange) -> impl IntoView { - let status = get_date_status(&date); + let status = date.get_status(); let status_text = match dbg!(status.clone()) { DateStatus::Blank => "", @@ -141,11 +108,12 @@ fn Date( /// ``` #[component] pub fn DateList( - #[prop(into)] dates: Vec<DateInfo>, + #[prop(into)] dates: Signal<Vec<DateInfo>>, ) -> impl IntoView { view! { {move || { dates + .get() .iter() .map(|info| { view! { <Date info=info.clone() /> } diff --git a/src/pages/api/mod.rs b/src/pages/api/mod.rs index e13ff2b..f7aa9bf 100644 --- a/src/pages/api/mod.rs +++ b/src/pages/api/mod.rs @@ -1,4 +1,7 @@ mod comparisons; +pub use comparisons::*; mod scholarships; +pub use scholarships::*; +mod dates; +pub use dates::*; -pub use self::{comparisons::*, scholarships::*}; diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index 546c41e..6000eea 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -1,10 +1,100 @@ use chrono::DateTime; use crate::common::{DateInfo, DateRange}; -use crate::components::{DashboardButton, DateList}; +use crate::components::{ActionButton, DashboardButton, DateList}; use leptos::prelude::*; +use crate::pages::api::{get_important_dates, CreateDates}; #[component] pub fn TestPage() -> impl IntoView { + let reload_signal = RwSignal::new(0); + + let dates_resource = Resource::new( + move || reload_signal.get(), + async move |_| { + get_important_dates().await + } + ); + + let dates_list = RwSignal::new(Vec::new()); + + Effect::new(move || { + if let Some(Ok(mut dates)) = dates_resource.get() { + dates.sort_by_key(|info| info.date.get_status()); + dates_list.set(dates); + } + }); + + let create_dates = ServerAction::<CreateDates>::new(); + + let on_click = move |_| { + let dates_list = vec![ + DateInfo { + id: uuid::Uuid::new_v4().to_string(), + title: "Open Example".to_string(), + date: DateRange::Range( + DateTime::parse_from_rfc3339("2026-01-15T07:15:00-05:00") + .unwrap_or_default(), + DateTime::parse_from_rfc3339("2026-01-30T14:05:00-05:00") + .unwrap_or_default() + ), + description: "This is a test".to_string(), + }, + DateInfo { + id: uuid::Uuid::new_v4().to_string(), + title: "Closed Example".to_string(), + date: DateRange::Range( + DateTime::parse_from_rfc3339("2026-01-01T07:15:00-05:00") + .unwrap_or_default(), + DateTime::parse_from_rfc3339("2026-01-14T14:05:00-05:00") + .unwrap_or_default() + ), + description: "".to_string(), + }, + DateInfo { + id: uuid::Uuid::new_v4().to_string(), + title: "Deadline Example".to_string(), + date: DateRange::Range( + DateTime::parse_from_rfc3339("2026-01-15T07:15:00-05:00") + .unwrap_or_default(), + DateTime::parse_from_rfc3339("2026-01-20T14:05:00-05:00") + .unwrap_or_default() + ), + description: "This is a test".to_string(), + }, + DateInfo { + id: uuid::Uuid::new_v4().to_string(), + title: "Upcoming Example".to_string(), + date: DateRange::Range( + DateTime::parse_from_rfc3339("2026-01-30T07:15:00-05:00") + .unwrap_or_default(), + DateTime::parse_from_rfc3339("2026-02-20T14:05:00-05:00") + .unwrap_or_default() + ), + description: "This is a test".to_string(), + }, + DateInfo { + id: uuid::Uuid::new_v4().to_string(), + title: "Single Date (Blank)".to_string(), + date: DateRange::Single( + DateTime::parse_from_rfc3339("2026-01-15T14:05:00-05:00") + .unwrap_or_default() + ), + description: "".to_string(), + } + ]; + + create_dates.dispatch(CreateDates { + dates: dates_list + }); + }; + + Effect::new(move || { + if let Some(Ok(_)) = create_dates.value().get() { + create_dates.clear(); + reload_signal.update(|v| *v += 1); + } + }); + view! { <div class="mx-auto px-6"> <div class="container mx-auto px-6 py-8 space-y-8"> @@ -36,60 +126,14 @@ pub fn TestPage() -> impl IntoView { </div> <div class="p-2"> <div class="flex flex-col p-3 gap-3"> - <DateList dates=vec![ - DateInfo { - title: "Open Example".to_string(), - date: DateRange::Range( - DateTime::parse_from_rfc3339("2026-01-15T07:15:00-05:00") - .unwrap_or_default(), - DateTime::parse_from_rfc3339("2026-01-30T14:05:00-05:00") - .unwrap_or_default() - ), - description: "This is a test".to_string(), - }, - DateInfo { - title: "Closed Example".to_string(), - date: DateRange::Range( - DateTime::parse_from_rfc3339("2026-01-01T07:15:00-05:00") - .unwrap_or_default(), - DateTime::parse_from_rfc3339("2026-01-14T14:05:00-05:00") - .unwrap_or_default() - ), - description: "".to_string(), - }, - DateInfo { - title: "Deadline Example".to_string(), - date: DateRange::Range( - DateTime::parse_from_rfc3339("2026-01-15T07:15:00-05:00") - .unwrap_or_default(), - DateTime::parse_from_rfc3339("2026-01-20T14:05:00-05:00") - .unwrap_or_default() - ), - description: "This is a test".to_string(), - }, - DateInfo { - title: "Upcoming Example".to_string(), - date: DateRange::Range( - DateTime::parse_from_rfc3339("2026-01-30T07:15:00-05:00") - .unwrap_or_default(), - DateTime::parse_from_rfc3339("2026-02-20T14:05:00-05:00") - .unwrap_or_default() - ), - description: "This is a test".to_string(), - }, - DateInfo { - title: "Single Date (Blank)".to_string(), - date: DateRange::Single( - DateTime::parse_from_rfc3339("2026-01-15T14:05:00-05:00") - .unwrap_or_default() - ), - description: "".to_string(), - } - ] /> + <DateList dates=dates_list /> </div> </div> </div> </section> + <section> + <ActionButton on:click=on_click>"Create Dates List"</ActionButton> + </section> </div> </div> </div> From bde15de634f9eaee2bde02aa8409532577bb8eed Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Fri, 16 Jan 2026 08:19:18 -0500 Subject: [PATCH 28/32] chore: remove testing dates list --- src/pages/test_page.rs | 95 +----------------------------------------- 1 file changed, 1 insertion(+), 94 deletions(-) diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index 6000eea..c38cbe6 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -5,96 +5,7 @@ use leptos::prelude::*; use crate::pages::api::{get_important_dates, CreateDates}; #[component] -pub fn TestPage() -> impl IntoView { - let reload_signal = RwSignal::new(0); - - let dates_resource = Resource::new( - move || reload_signal.get(), - async move |_| { - get_important_dates().await - } - ); - - let dates_list = RwSignal::new(Vec::new()); - - Effect::new(move || { - if let Some(Ok(mut dates)) = dates_resource.get() { - dates.sort_by_key(|info| info.date.get_status()); - dates_list.set(dates); - } - }); - - let create_dates = ServerAction::<CreateDates>::new(); - - let on_click = move |_| { - let dates_list = vec![ - DateInfo { - id: uuid::Uuid::new_v4().to_string(), - title: "Open Example".to_string(), - date: DateRange::Range( - DateTime::parse_from_rfc3339("2026-01-15T07:15:00-05:00") - .unwrap_or_default(), - DateTime::parse_from_rfc3339("2026-01-30T14:05:00-05:00") - .unwrap_or_default() - ), - description: "This is a test".to_string(), - }, - DateInfo { - id: uuid::Uuid::new_v4().to_string(), - title: "Closed Example".to_string(), - date: DateRange::Range( - DateTime::parse_from_rfc3339("2026-01-01T07:15:00-05:00") - .unwrap_or_default(), - DateTime::parse_from_rfc3339("2026-01-14T14:05:00-05:00") - .unwrap_or_default() - ), - description: "".to_string(), - }, - DateInfo { - id: uuid::Uuid::new_v4().to_string(), - title: "Deadline Example".to_string(), - date: DateRange::Range( - DateTime::parse_from_rfc3339("2026-01-15T07:15:00-05:00") - .unwrap_or_default(), - DateTime::parse_from_rfc3339("2026-01-20T14:05:00-05:00") - .unwrap_or_default() - ), - description: "This is a test".to_string(), - }, - DateInfo { - id: uuid::Uuid::new_v4().to_string(), - title: "Upcoming Example".to_string(), - date: DateRange::Range( - DateTime::parse_from_rfc3339("2026-01-30T07:15:00-05:00") - .unwrap_or_default(), - DateTime::parse_from_rfc3339("2026-02-20T14:05:00-05:00") - .unwrap_or_default() - ), - description: "This is a test".to_string(), - }, - DateInfo { - id: uuid::Uuid::new_v4().to_string(), - title: "Single Date (Blank)".to_string(), - date: DateRange::Single( - DateTime::parse_from_rfc3339("2026-01-15T14:05:00-05:00") - .unwrap_or_default() - ), - description: "".to_string(), - } - ]; - - create_dates.dispatch(CreateDates { - dates: dates_list - }); - }; - - Effect::new(move || { - if let Some(Ok(_)) = create_dates.value().get() { - create_dates.clear(); - reload_signal.update(|v| *v += 1); - } - }); - +pub fn TestPage() -> impl IntoView { view! { <div class="mx-auto px-6"> <div class="container mx-auto px-6 py-8 space-y-8"> @@ -126,14 +37,10 @@ pub fn TestPage() -> impl IntoView { </div> <div class="p-2"> <div class="flex flex-col p-3 gap-3"> - <DateList dates=dates_list /> </div> </div> </div> </section> - <section> - <ActionButton on:click=on_click>"Create Dates List"</ActionButton> - </section> </div> </div> </div> From 6105fa3bd5eb6d40ef279a3f38cb0502cf51a8d7 Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Fri, 16 Jan 2026 08:19:52 -0500 Subject: [PATCH 29/32] feat: add dates list to provider portal --- src/pages/provider_portal.rs | 87 ++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/src/pages/provider_portal.rs b/src/pages/provider_portal.rs index 145901b..8dc2018 100644 --- a/src/pages/provider_portal.rs +++ b/src/pages/provider_portal.rs @@ -1,42 +1,71 @@ -use crate::components::{Banner, DashboardButton}; +use crate::components::{Banner, DashboardButton, DateList, Loading}; use leptos::prelude::*; +use leptos_oidc::{AuthLoaded, Authenticated}; +use crate::pages::api::get_important_dates; +use crate::pages::UnauthenticatedPage; #[component] pub fn ProviderPortal() -> impl IntoView { + let dates_resource = OnceResource::new(async move { + get_important_dates().await + }); + + let dates_list = RwSignal::new(Vec::new()); + + Effect::new(move || { + if let Some(Ok(mut dates)) = dates_resource.get() { + dates.sort_by_key(|info| info.date.get_status()); + dates_list.set(dates); + } + }); + view! { <Banner title="Provider Dashboard" logo="PHS_Stacked_Acronym.png" path="/" /> - <div class="mx-auto px-6"> - <div class="container mx-auto px-6 py-8 space-y-8"> - <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> - <section class="space-y-4"> - <DashboardButton - title="Profile" - description="Edit user profile" - icon="/Person_Black.png" - path="/providers/profile" - /> - <DashboardButton - title="Create Scholarship" - description="Navigate to Scholarship Creation Page" - icon="/Create_Black.png" - path="/providers/scholarships" - /> - <DashboardButton - title="Applicants" - description="View Scholarship Applicants" - icon="/Form_Black.png" - path="/" - /> - </section> - <section class="space-y-4"> - - </section> + <AuthLoaded fallback=Loading> + <Authenticated unauthenticated=UnauthenticatedPage> + <div class="mx-auto px-6"> + <div class="container mx-auto px-6 py-8 space-y-8"> + <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> + <section class="space-y-4"> + <DashboardButton + title="Profile" + description="Edit user profile" + icon="/Person_Black.png" + path="/providers/profile" + /> + <DashboardButton + title="Create Scholarship" + description="Navigate to Scholarship Creation Page" + icon="/Create_Black.png" + path="/providers/scholarships" + /> + <DashboardButton + title="Applicants" + description="View Scholarship Applicants" + icon="/Form_Black.png" + path="/" + /> + </section> + <section class="space-y-4"> + <div class="rounded-lg shadow-lg/25 overflow-hidden"> + <div class="bg-red-900 text-white px-4 py-3 shadow-lg"> + <h3 class="text-white font-bold">Important Dates</h3> + </div> + <div class="p-2"> + <div class="flex flex-col p-3 gap-3"> + <DateList dates=dates_list /> + </div> + </div> + </div> + </section> + </div> + </div> </div> - </div> - </div> + </Authenticated> + </AuthLoaded> } } From 87aa10405582b21141d1d4eff978c6225ec7df98 Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Fri, 16 Jan 2026 08:20:05 -0500 Subject: [PATCH 30/32] feat: make banner text larger --- src/components/banner.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/banner.rs b/src/components/banner.rs index 79b95ca..ac7dce2 100644 --- a/src/components/banner.rs +++ b/src/components/banner.rs @@ -32,17 +32,21 @@ pub fn Banner( view! { <div class="mx-auto"> - <div class="bg-red-900 shadow-lg shadow-[inset_0_2px_4px_0_rgba(0,0,0,0.3),0_4px_6px_-1px_rgba(127,29,29,0.3),0_2px_4px_-1px_rgba(127,29,29,0.2)]"> - <div class="flex items-center gap-4 p-6"> - <div class="flex-shrink-0"> - <img src={logo.clone()} class="h-10 w-10 cursor-pointer" alt="icon" on:click=on_click/> - </div> - <div class="flex-1"> - <h2 class="text-white text-lg font-bold">{title.clone()}</h2> - </div> + <div class="bg-red-900 shadow-lg shadow-[inset_0_2px_4px_0_rgba(0,0,0,0.3),0_4px_6px_-1px_rgba(127,29,29,0.3),0_2px_4px_-1px_rgba(127,29,29,0.2)]"> + <div class="flex items-center gap-4 p-6"> + <div class="flex-shrink-0"> + <img + src=logo.clone() + class="h-10 w-10 cursor-pointer" + alt="icon" + on:click=on_click + /> + </div> + <div class="flex-1"> + <h2 class="text-white text-2xl font-bold">{title.clone()}</h2> + </div> + </div> </div> - </div> </div> - } } From c3dbe3e2ac026ad2df13b4e94e6645423b532f5d Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Fri, 16 Jan 2026 08:20:31 -0500 Subject: [PATCH 31/32] style: remove annoying indent --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index bb3064d..047c046 100644 --- a/src/app.rs +++ b/src/app.rs @@ -65,7 +65,7 @@ pub fn AppWithRoutes() -> impl IntoView { <Route path=path!("/about") view=AboutPage /> <Route path=path!("/test_page") view=TestPage /> <Route path=path!("/comparison") view=ComparisonTestPage /> - <ProviderRoutes /> + <ProviderRoutes /> <LoanerRoutes /> </Routes> </ToastList> From 84212d2360d65acc3949677ecea56f5a84817234 Mon Sep 17 00:00:00 2001 From: Lucas Vas <lvas463@gmail.com> Date: Fri, 16 Jan 2026 08:54:41 -0500 Subject: [PATCH 32/32] fix: apply into_any on pages that will not compile This is a production-only build failure due to type explosion. --- src/components/dashboard_button.rs | 2 +- src/components/date.rs | 2 +- src/components/lists/single/radio.rs | 43 ++++++++++++++-------------- src/pages/home_page.rs | 4 +-- src/pages/scholarship_info.rs | 2 +- src/pages/test_page.rs | 7 ++--- 6 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/components/dashboard_button.rs b/src/components/dashboard_button.rs index f471e33..9558824 100644 --- a/src/components/dashboard_button.rs +++ b/src/components/dashboard_button.rs @@ -57,5 +57,5 @@ pub fn DashboardButton( </div> </button> - } + }.into_any() } diff --git a/src/components/date.rs b/src/components/date.rs index 88c66f8..67c50d8 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -120,5 +120,5 @@ pub fn DateList( }) .collect_view() }} - } + }.into_any() } diff --git a/src/components/lists/single/radio.rs b/src/components/lists/single/radio.rs index ebaaa01..a554dd7 100644 --- a/src/components/lists/single/radio.rs +++ b/src/components/lists/single/radio.rs @@ -108,22 +108,21 @@ fn RadioOther( // This div displays the box for the checkbox, and applies some styling to the child // span element. <div class="relative inline-block min-h-5 min-w-5 h-5 w-5 m-1 mr-2 rounded-full border border-gray-300 bg-white - transition-bg duration-200 peer-checked:bg-red-700 peer-checked:border-red-700 - [&>span]:opacity-0 peer-checked:[&>span]:opacity-100 - peer-disabled:bg-gray-600/33 peer-disabled:border-gray-600 - peer-disabled:peer-checked:bg-gray-600"> + transition-bg duration-200 peer-checked:bg-red-700 peer-checked:border-red-700 + [&>span]:opacity-0 peer-checked:[&>span]:opacity-100 + peer-disabled:bg-gray-600/33 peer-disabled:border-gray-600 + peer-disabled:peer-checked:bg-gray-600"> // This span is the check icon. It is centered within the div and has a border on the bottom // and right sides, which is then rotated to appear like a checkmark. - <span - class="absolute left-1/2 top-1/2 block h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 - rotate-45 transform border-3 border-3 rounded-full bg-white - border-white transition-opacity duration-150"></span> + <span class="absolute left-1/2 top-1/2 block h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 + rotate-45 transform border-3 border-3 rounded-full bg-white + border-white transition-opacity duration-150"></span> </div> <input node_ref=input_ref class="border-b-2 mb-1.5 px-1.5 py-0.5 rounded-t-xs bg-transparent relative flex-1 transition-all duration-150 - border-red-700 disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" + border-red-700 disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" r#type="text" disabled=input_disabled placeholder=placeholder @@ -243,8 +242,6 @@ pub fn RadioList( selected_value.get() == item.get() && !other_checked.get() }); let on_change = move || { - // on_change only runs when the radio button is checked, not when unchecked. - // Change the selected value to the new one. other_checked.set(false); selected_value.set(item.get()); error @@ -256,6 +253,8 @@ pub fn RadioList( ); dirty.set(true); }; + // on_change only runs when the radio button is checked, not when unchecked. + // Change the selected value to the new one. view! { <Radio @@ -270,17 +269,17 @@ pub fn RadioList( }) .collect::<Vec<_>>()} - <Show when=move || !other_prompt.get().is_empty()> - <RadioOther - checked=other_checked - name=data_member - disabled=disabled - value=other_value - on_change=move |_| other_checked.set(true) - placeholder=other_prompt - dirty=dirty - /> - </Show> + <Show when=move || !other_prompt.get().is_empty()> + <RadioOther + checked=other_checked + name=data_member + disabled=disabled + value=other_value + on_change=move |_| other_checked.set(true) + placeholder=other_prompt + dirty=dirty + /> + </Show> </div> <Show when=move || show_errors.get()> <div class="text-red-600 text-sm mr-1.5 ml-1.5"> diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 748e088..194c44f 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -24,7 +24,6 @@ use leptos::task::spawn_local; use leptos::web_sys::HtmlAnchorElement; use leptos::wasm_bindgen::JsCast; use std::collections::HashMap; -use crate::pages::api::get_comparisons_categorized; use crate::utils::get_user_claims; /// # Get Student Info @@ -363,8 +362,7 @@ pub fn HomePage() -> impl IntoView { </Row> } }) - }} - <Row> + }} <Row> <ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton> </Row> </Suspense> diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index 4da0253..7a0628d 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -609,7 +609,7 @@ fn ScholarshipForm( items=vec![ "Write an essay describing your major/career direction. Include any abilities, interests, experiences, employment or clubs you have had that have affected your direction. If you are undecided, indicate why.", "Why is community service important? How has it personally affected you and your goals and what role do you hope to play in it in the future?", - "How has participation in sports been beneficial to your personal, academic, and emotional growth?" + "How has participation in sports been beneficial to your personal, academic, and emotional growth?", ] .iter() .map(|prompt| prompt.to_string()) diff --git a/src/pages/test_page.rs b/src/pages/test_page.rs index c38cbe6..008676f 100644 --- a/src/pages/test_page.rs +++ b/src/pages/test_page.rs @@ -1,8 +1,5 @@ -use chrono::DateTime; -use crate::common::{DateInfo, DateRange}; -use crate::components::{ActionButton, DashboardButton, DateList}; +use crate::components::DashboardButton; use leptos::prelude::*; -use crate::pages::api::{get_important_dates, CreateDates}; #[component] pub fn TestPage() -> impl IntoView { @@ -44,5 +41,5 @@ pub fn TestPage() -> impl IntoView { </div> </div> </div> - } + }.into_any() }