Merge pull request #50 from R15-Software-Dev/feat/date-component

Add date list component
This commit was merged in pull request #50.
This commit is contained in:
Lucas Vas authored and GitHub committed 2026-01-16 09:02:18 -05:00
commit 9ebab0689a
37 files changed
+762 -274

No files matched your search

+32 -48
View File
@@ -1,8 +1,10 @@
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::prelude::*;
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context}; use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
use leptos_oidc::{Auth, AuthParameters, AuthSignal, Challenge}; use leptos_oidc::AuthSignal;
use leptos_router::{components::{Route, Router, Routes, ParentRoute}, path}; 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; use crate::components::ToastList;
pub fn shell(options: LeptosOptions) -> impl IntoView { 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] #[component]
pub fn AppWithRoutes() -> impl IntoView { pub fn AppWithRoutes() -> impl IntoView {
provide_meta_context(); provide_meta_context();
let current_origin = use_origin();
// Authentication setup
let parameters = AuthParameters {
issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM".into(),
client_id: "10jr2h3vtpu9n7gj46pvg5qo2q".into(),
redirect_uri: format!("{}/", use_origin()),
post_logout_redirect_uri: current_origin,
scope: Some("openid%20profile%20email".into()),
audience: None,
challenge: Challenge::None,
};
let auth = Auth::signal();
provide_context(auth); // allows use of this signal in lower areas of the tree without
// explicitly passing it through the html tree
let _ = Auth::init(parameters);
view! { view! {
// injects a stylesheet into the document <head> // injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet // id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css" /> <Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css" />
// <AuthLoading><p>"Authentication is loading"</p></AuthLoading>
// <AuthErrorContext><AuthErrorView/></AuthErrorContext>
// sets the document title // sets the document title
<Title text="R15 Scholarship App DEV" /> <Title text="R15 Scholarship App DEV" />
@@ -102,21 +65,42 @@ pub fn AppWithRoutes() -> impl IntoView {
<Route path=path!("/about") view=AboutPage /> <Route path=path!("/about") view=AboutPage />
<Route path=path!("/test_page") view=TestPage /> <Route path=path!("/test_page") view=TestPage />
<Route path=path!("/comparison") view=ComparisonTestPage /> <Route path=path!("/comparison") view=ComparisonTestPage />
<Route path=path!("/providers") view=ProviderPortal/> <ProviderRoutes />
<Route path=path!("/providers/profile") view=ProviderContactPage /> <LoanerRoutes />
<ParentRoute path=path!("/providers/scholarships") view=ScholarshipInfoPage> </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!(":id") view=ScholarshipInfoPage />
<Route path=path!("") view=ScholarshipInfoPage /> <Route path=path!("") view=ScholarshipInfoPage />
</ParentRoute> </ParentRoute>
</ParentRoute>
}
.into_inner()
.into_any_nested_route()
}
#[component(transparent)]
fn LoanerRoutes() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("loaners") view=LoanerShell> <ParentRoute path=path!("loaners") view=LoanerShell>
<Route path=path!("") view=LoanerFallback /> <Route path=path!("") view=LoanerFallback />
<Route path=path!("borrowing") view=LoanerBorrowForm /> <Route path=path!("borrowing") view=LoanerBorrowForm />
<Route path=path!("returning") view=LoanerReturnForm /> <Route path=path!("returning") view=LoanerReturnForm />
</ParentRoute> </ParentRoute>
</Routes>
</ToastList>
</main>
} }
.into_inner()
.into_any_nested_route()
} }
#[component] #[component]
+63
View File
@@ -0,0 +1,63 @@
use chrono::{DateTime, FixedOffset, Local};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DateInfo {
pub id: String,
pub title: String,
pub date: DateRange,
pub description: String,
}
impl DateInfo {
pub fn get_start_date(&self) -> DateTime<FixedOffset> {
match self.date {
DateRange::Single(date) | DateRange::Range(date, _) => date,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DateRange {
Single(DateTime<FixedOffset>),
Range(DateTime<FixedOffset>, DateTime<FixedOffset>),
}
impl DateRange {
pub fn get_status(&self) -> DateStatus {
match self {
DateRange::Single(_) => DateStatus::Blank,
DateRange::Range(start_date, end_date) => {
let local = Local::now();
// Check the amount of time from now to the start date. Values that are negative
// indicate that the start date has already passed.
if dbg!(start_date.signed_duration_since(local).num_days()) > 0 {
DateStatus::Upcoming
} else {
// Check the amount of time from now to the end date. The value will be positive if
// the end date is in the future. We will be specific to the minute.
let duration_until = dbg!(end_date).signed_duration_since(local);
let num_days = dbg!(duration_until.num_days());
let num_mins = dbg!(duration_until.num_minutes());
if num_mins <= 0 {
DateStatus::Closed
} else if num_days <= 5 {
DateStatus::Deadline
} else {
DateStatus::Open
}
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
pub enum DateStatus {
Closed,
Deadline,
Open,
Upcoming,
Blank,
}
+1 -1
View File
@@ -15,7 +15,7 @@ use std::collections::HashMap;
/// now, the table will simply create a single massive `Map` field that contains all the stored /// now, the table will simply create a single massive `Map` field that contains all the stored
/// information in the `HashMap`. We'd prefer to be able to get single values if possible, /// information in the `HashMap`. We'd prefer to be able to get single values if possible,
/// especially for interoperability with other applications, should they be created. /// especially for interoperability with other applications, should they be created.
#[derive(Debug, Clone, Serialize, Deserialize, Reactive, )] #[derive(Debug, Clone, Serialize, Deserialize, Reactive)]
pub struct ExpandableInfo { pub struct ExpandableInfo {
/// The main key member. Required and used to get/put the information in a database. /// The main key member. Required and used to get/put the information in a database.
pub subject: String, pub subject: String,
+6 -2
View File
@@ -1,10 +1,14 @@
mod comparison; mod comparison;
mod date_info;
mod expandable_info; mod expandable_info;
mod form_params; mod form_params;
mod form_schema; mod form_schema;
mod submit_status;
mod user; mod user;
mod value_type; mod value_type;
mod submit_status;
pub mod animations; pub mod animations;
pub use self::{comparison::*, expandable_info::*, form_schema::*, user::*, value_type::*, form_params::*, submit_status::*}; pub use self::{
comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*,
submit_status::*, user::*, value_type::*,
};
+7 -3
View File
@@ -35,14 +35,18 @@ pub fn Banner(
<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="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 items-center gap-4 p-6">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<img src={logo.clone()} class="h-10 w-10 cursor-pointer" alt="icon" on:click=on_click/> <img
src=logo.clone()
class="h-10 w-10 cursor-pointer"
alt="icon"
on:click=on_click
/>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<h2 class="text-white text-lg font-bold">{title.clone()}</h2> <h2 class="text-white text-2xl font-bold">{title.clone()}</h2>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
} }
} }
+1 -1
View File
@@ -57,5 +57,5 @@ pub fn DashboardButton(
</div> </div>
</button> </button>
} }.into_any()
} }
+124
View File
@@ -0,0 +1,124 @@
use crate::common::{DateInfo, DateRange, DateStatus};
use leptos::prelude::*;
fn render_dates(dates: DateRange) -> impl IntoView {
let date_text = match dates {
DateRange::Single(date) => format!("{}", date.format("%A %B %d, %I:%M%p")),
DateRange::Range(start_date, end_date) =>
format!("{} - {}",
start_date.format("%D %I:%M%p"),
end_date.format("%D %I:%M%p")
),
};
view! { <span>{move || date_text.clone()}</span> }
}
fn render_status(date: DateRange) -> impl IntoView {
let status = date.get_status();
let status_text = match dbg!(status.clone()) {
DateStatus::Blank => "",
DateStatus::Upcoming => "Upcoming",
DateStatus::Open => "Open",
DateStatus::Deadline => "Deadline",
DateStatus::Closed => "Closed",
};
let background_class = match status {
DateStatus::Blank => "hidden border-none",
DateStatus::Upcoming => "bg-yellow-100 text-yellow-800 border-yellow-100",
DateStatus::Open => "bg-green-100 text-green-800 border-green-100",
DateStatus::Deadline => "bg-red-100 text-red-800 border-red-100",
DateStatus::Closed => "bg-red-700 text-white border-red-700"
};
let classes =
format!("inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs
font-medium w-fit whitespace-nowrap shrink-0 {}",
background_class
);
view! { <span class=classes>{move || status_text}</span> }
}
/// # Date Component
/// A single date, for use in a [`DateList`] component.
///
/// Example usage:
/// ```
/// let date_info = DateInfo {
/// title: "Example Title".to_string(),
/// date: DateRange::Single("January 1st, 1990".to_string()),
/// description: "A description of the date.".to_string(),
/// status: DateStatus::Upcoming
/// }
///
/// view! {
/// <Date
/// info=date_info
/// />
/// }
/// ```
#[component]
fn Date(
/// Struct of title, date, and description
#[prop(into)] info: Signal<DateInfo>,
) -> impl IntoView {
view! {
<div class="flex items-start gap-3 rounded-lg border-grey-300 p-3
w-full text-left shadow-lg">
// <div class="flex-shrink-0 mt-0.5">
// <img src={icon} class="h-4 w-4" alt="icon"/>
// </div>
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="font-bold text-md text-base">{info.get().title}</h3>
<h3 class="text-md text-base mt-1">
{move || render_dates(info.get().date)}
</h3>
<p class="text-sm text-gray-600">{info.get().description}</p>
</div>
<div class="flex-shrink-0 ml-2">{move || render_status(info.get().date)}</div>
</div>
</div>
</div>
}
}
///# Dates List Component
/// Displays a list of important dates in a column, with badges that draw attention to dates that
/// are more important.
///
/// Example usage:
/// ```
/// let dates = vec![
/// DateInfo {
/// title: "Example Title".to_string(),
/// date: DateRange::Single("January 1st, 1990".to_string()),
/// description: "A description of the date.".to_string(),
/// status: DateStatus::Upcoming
/// }
/// ]
///
/// view! {
/// <DateList dates=dates />
/// }
/// ```
#[component]
pub fn DateList(
#[prop(into)] dates: Signal<Vec<DateInfo>>,
) -> impl IntoView {
view! {
{move || {
dates
.get()
.iter()
.map(|info| {
view! { <Date info=info.clone() /> }
})
.collect_view()
}}
}.into_any()
}
+7 -6
View File
@@ -134,15 +134,13 @@ pub fn CheckboxList(
let cloned = item.clone(); let cloned = item.clone();
move || cloned.clone() move || cloned.clone()
}); });
let checked = Signal::derive(move || { let checked = Signal::derive(move || {
controller.selected_list.get().contains(&item.get()) controller.selected_list.get().contains(&item.get())
}); });
let on_change = move || { let on_change = move || {
// Update the data list by checking if the value is present in it. If so, controller
// remove it. If not, add it. .selected_list
controller.selected_list.update(|list| { .update(|list| {
let item = item.get(); let item = item.get();
if list.contains(&item) { if list.contains(&item) {
list.retain(|val| *val != item); list.retain(|val| *val != item);
@@ -153,6 +151,9 @@ pub fn CheckboxList(
controller.dirty.set(true); 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! { view! {
<Checkbox <Checkbox
checked=checked checked=checked
@@ -171,7 +172,7 @@ pub fn CheckboxList(
{move || { {move || {
match controller.error.get() { match controller.error.get() {
ValidationState::Invalid(msg) => msg, 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> </div>
+29 -15
View File
@@ -45,7 +45,8 @@ pub fn Chip(
id=id.clone() id=id.clone()
prop:checked=checked prop:checked=checked
on:change=move |_| on_change.run(()) on:change=move |_| on_change.run(())
disabled=disabled /> disabled=disabled
/>
<div class="rounded-full p-2.5 <div class="rounded-full p-2.5
cursor-pointer text-base cursor-pointer text-base
bg-transparent border-1 border-gray-300 bg-transparent border-1 border-gray-300
@@ -98,34 +99,47 @@ pub fn ChipsList(
<div class="m-1.5 mt-0 mb-0"> <div class="m-1.5 mt-0 mb-0">
<span class="font-bold">{label}</span> <span class="font-bold">{label}</span>
<div class="flex flex-row flex-wrap gap-2 mb-1"> <div class="flex flex-row flex-wrap gap-2 mb-1">
{move || displayed_text.get().into_iter().zip(values.get()) {move || {
displayed_text
.get()
.into_iter()
.zip(values.get())
.into_iter() .into_iter()
.map(|(text, value)| { .map(|(text, value)| {
let value = RwSignal::new(value); let value = RwSignal::new(value);
let checked = Signal::derive(move || { let checked = Signal::derive(move || {
controller.selected_list.get().contains(&value.get()) controller.selected_list.get().contains(&value.get())
}); });
let on_change = move || { 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 value = value.get();
let list = controller.selected_list.get(); let list = controller.selected_list.get();
if list.contains(&value) { if list.contains(&value) {
allows_multiple.get() allows_multiple
.then(|| controller.selected_list.update(|list| list.retain(|v| v != &value))) .get()
.then(|| {
controller
.selected_list
.update(|list| list.retain(|v| v != &value))
})
.or_else(|| Some(controller.selected_list.set(Vec::new()))); .or_else(|| Some(controller.selected_list.set(Vec::new())));
} else { } else {
allows_multiple.get() allows_multiple
.then(|| controller.selected_list.update(|list| list.push(value.clone()))) .get()
.or_else(|| Some(controller.selected_list.set(vec![value.clone()]))); .then(|| {
controller
.selected_list
.update(|list| list.push(value.clone()))
})
.or_else(|| Some(
controller.selected_list.set(vec![value.clone()]),
));
} }
controller.dirty.set(true); 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! { view! {
<Chip <Chip
checked=checked checked=checked
@@ -138,7 +152,7 @@ pub fn ChipsList(
} }
}) })
.collect_view() .collect_view()
} }}
</div> </div>
</div> </div>
<Show when=move || controller.show_errors.get()> <Show when=move || controller.show_errors.get()>
@@ -146,7 +160,7 @@ pub fn ChipsList(
{move || { {move || {
match controller.error.get() { match controller.error.get() {
ValidationState::Invalid(msg) => msg, 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> </div>
+11 -8
View File
@@ -115,8 +115,7 @@ fn RadioOther(
// This span is the check icon. It is centered within the div and has a border on the bottom // 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. // and right sides, which is then rotated to appear like a checkmark.
<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
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 rotate-45 transform border-3 border-3 rounded-full bg-white
border-white transition-opacity duration-150"></span> border-white transition-opacity duration-150"></span>
</div> </div>
@@ -239,19 +238,23 @@ pub fn RadioList(
.into_iter() .into_iter()
.map(|item| { .map(|item| {
let item = RwSignal::new(item); let item = RwSignal::new(item);
let checked_signal = Signal::derive(move || { let checked_signal = Signal::derive(move || {
selected_value.get() == item.get() && !other_checked.get() selected_value.get() == item.get() && !other_checked.get()
}); });
let on_change = move || { 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); other_checked.set(false);
selected_value.set(item.get()); 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); 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! { view! {
<Radio <Radio
@@ -283,7 +286,7 @@ pub fn RadioList(
{move || { {move || {
match error.get() { match error.get() {
ValidationState::Invalid(msg) => msg, 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> </div>
+11 -8
View File
@@ -104,15 +104,18 @@ pub fn Select(
value="" value=""
disabled disabled
hidden hidden
selected={move || raw_value.get().is_empty()} selected=move || raw_value.get().is_empty()
>"Select one..."</option> >
"Select one..."
</option>
{move || { {move || {
value_list.iter().map( value_list
move |value| { .iter()
.map(move |value| {
let display = value.to_owned(); let display = value.to_owned();
view! { <option value={display}>{display.clone()}</option> } view! { <option value=display>{display.clone()}</option> }
} })
).collect::<Vec<_>>() .collect::<Vec<_>>()
}} }}
</select> </select>
</label> </label>
@@ -122,7 +125,7 @@ pub fn Select(
{move || { {move || {
match error.get() { match error.get() {
ValidationState::Invalid(msg) => msg, 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> </div>
+5
View File
@@ -0,0 +1,5 @@
mod provider;
pub use provider::*;
mod student;
pub use student::*;
pub(crate) mod setup;
+30
View File
@@ -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 />
}
}
+24
View File
@@ -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);
}
+30
View File
@@ -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 />
}
}
+3 -1
View File
@@ -1,6 +1,7 @@
mod action_button; mod action_button;
mod banner; mod banner;
mod dashboard_button; mod dashboard_button;
mod date;
mod loading; mod loading;
mod multi_entry; mod multi_entry;
mod panel; mod panel;
@@ -11,9 +12,10 @@ mod validated_form;
mod lists; mod lists;
mod toasts; mod toasts;
mod header; mod header;
pub mod login;
pub use self::{ pub use self::{
action_button::*, banner::*, dashboard_button::*, lists::*, action_button::*, banner::*, dashboard_button::*, lists::*,
loading::*, multi_entry::*, panel::*, row::*, text_field::*, loading::*, multi_entry::*, panel::*, row::*, text_field::*,
validated_form::*, toasts::*, header::* validated_form::*, toasts::*, header::*, date::*
}; };
+4 -4
View File
@@ -147,9 +147,9 @@ pub fn OutlinedTextField(
transition-all duration-150 transition-all duration-150
border-red-700 bg-transparent border-red-700 bg-transparent
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
r#type={str_type} r#type=str_type
disabled={disabled} disabled=disabled
placeholder={placeholder} placeholder=placeholder
prop:name=name prop:name=name
prop:value=raw_value prop:value=raw_value
on:input=on_input on:input=on_input
@@ -160,7 +160,7 @@ pub fn OutlinedTextField(
{move || { {move || {
match error.get() { match error.get() {
ValidationState::Invalid(msg) => msg, 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> </div>
+1
View File
@@ -2,6 +2,7 @@ pub mod app;
mod common; mod common;
mod components; mod components;
mod pages; mod pages;
pub mod utils;
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen] #[wasm_bindgen::prelude::wasm_bindgen]
+17 -2
View File
@@ -6,6 +6,11 @@ async fn main() {
use leptos::prelude::*; use leptos::prelude::*;
use leptos_axum::{LeptosRoutes, generate_route_list}; use leptos_axum::{LeptosRoutes, generate_route_list};
use scholarships_rs::app::*; 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 conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr; let addr = conf.leptos_options.site_addr;
@@ -14,10 +19,20 @@ async fn main() {
let routes = generate_route_list(App); let routes = generate_route_list(App);
let app = Router::new() let app = Router::new()
.leptos_routes(&leptos_options, routes, { .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(); let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone()) move || shell(leptos_options.clone())
}) }
)
.fallback(leptos_axum::file_and_error_handler(shell)) .fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options); .with_state(leptos_options);
+2 -2
View File
@@ -1,6 +1,5 @@
use std::collections::HashMap;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use crate::pages::utils::server_utils::create_dynamo_client; use crate::utils::server::create_dynamo_client;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use aws_sdk_dynamodb::error::ProvideErrorMetadata; use aws_sdk_dynamodb::error::ProvideErrorMetadata;
@@ -12,6 +11,7 @@ use crate::common::{
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison, ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
TextComparison, TextListComparison, ValueType, TextComparison, TextListComparison, ValueType,
}; };
use std::collections::HashMap;
use leptos::prelude::ServerFnError; use leptos::prelude::ServerFnError;
use leptos::server; use leptos::server;
+75
View File
@@ -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)
}
+4 -1
View File
@@ -1,4 +1,7 @@
mod comparisons; mod comparisons;
pub use comparisons::*;
mod scholarships; mod scholarships;
pub use scholarships::*;
mod dates;
pub use dates::*;
pub use self::{comparisons::*, scholarships::*};
+1 -2
View File
@@ -8,7 +8,7 @@ use leptos::leptos_dom::log;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
use uuid::Uuid; use uuid::Uuid;
#[cfg(feature = "ssr")] #[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")] #[cfg(feature = "ssr")]
use crate::common::ValueType; use crate::common::ValueType;
@@ -52,7 +52,6 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
#[server(CreateScholarshipInfo, endpoint = "/scholarship/info/create")] #[server(CreateScholarshipInfo, endpoint = "/scholarship/info/create")]
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> { 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; use aws_sdk_dynamodb::error::ProvideErrorMetadata;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
+37
View File
@@ -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>
}
}
+32 -43
View File
@@ -1,16 +1,14 @@
use super::UnauthenticatedPage; use super::UnauthenticatedPage;
use super::api::{get_all_scholarship_info, get_comparison_info};
use super::home_page::get_submission; use super::home_page::get_submission;
use crate::common::{ use crate::common::{ComparisonData, ExpandableInfo, ValueType};
ComparisonData, ExpandableInfo, ValueType,
};
use crate::components::{ActionButton, Loading, Panel, Row}; use crate::components::{ActionButton, Loading, Panel, Row};
use leptos::either::Either; use leptos::either::Either;
use leptos::leptos_dom::logging::console_log; use leptos::leptos_dom::logging::console_log;
use leptos::logging::log; use leptos::logging::log;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::{AuthLoaded, Authenticated}; use leptos_oidc::{AuthLoaded, Authenticated};
use crate::pages::utils::get_user_claims; use crate::utils::get_user_claims;
use super::api::{get_comparison_info, get_all_scholarship_info};
#[component] #[component]
pub fn ComparisonTestPage() -> impl IntoView { pub fn ComparisonTestPage() -> impl IntoView {
@@ -59,51 +57,42 @@ pub fn ComparisonTestPage() -> impl IntoView {
let student_info = student_resource.get(); let student_info = student_resource.get();
let comparison_info = comparison_resource.get(); let comparison_info = comparison_resource.get();
let scholarship_info = scholarship_resource.get(); let scholarship_info = scholarship_resource.get();
match (student_info, comparison_info, scholarship_info) { match (student_info, comparison_info, scholarship_info) {
(Some(student), Some(comparisons), Some(scholarships)) => Either::Right(view! { (Some(student), Some(comparisons), Some(scholarships)) => {
Either::Right(
view! {
<Panel> <Panel>
<Row> <Row>
<p>{result_msg}</p> <p>{result_msg}</p>
</Row> </Row>
<Row> <Row>
<ActionButton on:click=move |_| { <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 comp_member = "requirements".to_string();
let comparison_results = scholarships.iter().map(|scholarship| { let comparison_results = scholarships
.iter()
.map(|scholarship| {
let list = match scholarship.data.get(&comp_member) { let list = match scholarship.data.get(&comp_member) {
Some(ValueType::List(Some(list))) => list, Some(ValueType::List(Some(list))) => list,
_ => { _ => {
log!("Cannot find requirements, assuming none."); log!("Cannot find requirements, assuming none.");
return Ok(Some(scholarship)) // students will always qualify when there are no requirements return Ok(Some(scholarship));
} }
}; };
let mut final_result = Ok(true); let mut final_result = Ok(true);
// The list should exist, and it should contain a series of strings.
for val in list { for val in list {
let comp_opt = match val { let comp_opt = match val {
ValueType::String(Some(comp_id)) => { ValueType::String(Some(comp_id)) => {
// Find the comparison
comparisons.iter().find(|comp| comp.id == *comp_id) comparisons.iter().find(|comp| comp.id == *comp_id)
} }
_ => None _ => None,
}; };
let comparison_outcome = match comp_opt { let comparison_outcome = match comp_opt {
Some(comp) => { Some(comp) => comp.compare(&student),
// Perform the comparison _ => Err("".to_string()),
comp.compare(&student)
}
_ => Err("".to_string())
}; };
match comparison_outcome { match comparison_outcome {
Ok(true) => {}, Ok(true) => {}
Ok(false) => final_result = Ok(false), Ok(false) => final_result = Ok(false),
Err(e) => { Err(e) => {
final_result = Err(e); final_result = Err(e);
@@ -111,41 +100,41 @@ pub fn ComparisonTestPage() -> impl IntoView {
} }
} }
} }
match final_result { match final_result {
Ok(true) => Ok(Some(scholarship)), Ok(true) => Ok(Some(scholarship)),
Ok(false) => Ok(None), Ok(false) => Ok(None),
Err(e) => Err(e) Err(e) => Err(e),
} }
}).collect::<Vec<Result<Option<&ExpandableInfo>, String>>>(); })
.collect::<Vec<Result<Option<&ExpandableInfo>, String>>>();
// Build a display string
let mut strings = Vec::new(); let mut strings = Vec::new();
strings.push("Scholarship results: \n".to_string()); strings.push("Scholarship results: \n".to_string());
for result in comparison_results { for result in comparison_results {
match result { match result {
Ok(Some(scholarship_info)) => { Ok(Some(scholarship_info)) => {
// Get the scholarship name.
let name_member = "name".to_string(); let name_member = "name".to_string();
let scl_name = scholarship_info.data.get(&name_member).unwrap() let scl_name = scholarship_info
.as_string().unwrap().unwrap(); .data
.get(&name_member)
.unwrap()
.as_string()
.unwrap()
.unwrap();
strings.push(scl_name); strings.push(scl_name);
}, }
Ok(None) => {}, Ok(None) => {}
Err(msg) => strings.push(msg) Err(msg) => strings.push(msg),
} }
} }
let display_text = strings.join("\n").to_string(); let display_text = strings.join("\n").to_string();
result_msg.set(display_text); result_msg.set(display_text);
}>"Test Comparison"</ActionButton> }>"Test Comparison"</ActionButton>
</Row> </Row>
</Panel> </Panel>
}), },
_ => Either::Left(view! { )
<p>"Failed API requests."</p> }
}) _ => Either::Left(view! { <p>"Failed API requests."</p> }),
} }
}} }}
</Suspense> </Suspense>
+13 -7
View File
@@ -10,8 +10,10 @@ use std::io::Write;
use crate::common::ExpandableInfo; use crate::common::ExpandableInfo;
use crate::pages::UnauthenticatedPage; use crate::pages::UnauthenticatedPage;
use crate::pages::utils::get_user_claims; use crate::components::{
use crate::components::{ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm}; ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
Select, TextFieldType, ValidatedForm
};
use crate::input; use crate::input;
use leptos::leptos_dom::logging::console_log; use leptos::leptos_dom::logging::console_log;
use leptos::prelude::*; use leptos::prelude::*;
@@ -22,6 +24,7 @@ use leptos::task::spawn_local;
use leptos::web_sys::HtmlAnchorElement; use leptos::web_sys::HtmlAnchorElement;
use leptos::wasm_bindgen::JsCast; use leptos::wasm_bindgen::JsCast;
use std::collections::HashMap; use std::collections::HashMap;
use crate::utils::get_user_claims;
/// # Get Student Info /// # Get Student Info
/// Gets a student's information given their `subject`. /// Gets a student's information given their `subject`.
@@ -263,7 +266,7 @@ pub fn HomePage() -> impl IntoView {
data_member="contact_email" data_member="contact_email"
data_map=form_data data_map=form_data
input_type=TextFieldType::Email( input_type=TextFieldType::Email(
vec!["fakeemail.org".to_string(), "gmail.com".to_string()] vec!["fakeemail.org".to_string(), "gmail.com".to_string()],
) )
required=true required=true
/> />
@@ -344,8 +347,12 @@ pub fn HomePage() -> impl IntoView {
data_map=form_data data_map=form_data
data_member="community_involvement" data_member="community_involvement"
schema=vec![ schema=vec![
input!(Text, "service_name", "Activity Name:", true, "Some service activity..."), input!(
input!(Number, "service_hours", "Total Service Hours", true, "20") Text, "service_name", "Activity Name:", true, "Some service activity..."
),
input!(
Number, "service_hours", "Total Service Hours", true, "20"
),
] ]
/> />
</Row> </Row>
@@ -355,8 +362,7 @@ pub fn HomePage() -> impl IntoView {
</Row> </Row>
} }
}) })
}} }} <Row>
<Row>
<ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton> <ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton>
</Row> </Row>
</Suspense> </Suspense>
+44 -39
View File
@@ -213,11 +213,7 @@ pub fn LoanerShell() -> impl IntoView {
// We'll create the page here - the sidebar contains all the buttons, while the form // We'll create the page here - the sidebar contains all the buttons, while the form
// container either shows forms or shows placeholder text. // container either shows forms or shows placeholder text.
view! { view! {
<Banner <Banner title="Chromebook Loaners" logo="/PHS_Stacked_Acronym.png" path="/loaners" />
title="Chromebook Loaners"
logo="/PHS_Stacked_Acronym.png"
path="/loaners"
/>
<div class="flex m-5"> <div class="flex m-5">
<div class="flex flex-1 flex-row gap-3"> <div class="flex flex-1 flex-row gap-3">
<div id="sidebar" class="flex flex-1 flex-col gap-2"> <div id="sidebar" class="flex flex-1 flex-col gap-2">
@@ -247,16 +243,9 @@ pub fn LoanerShell() -> impl IntoView {
#[component] #[component]
pub fn LoanerFallback() -> impl IntoView { pub fn LoanerFallback() -> impl IntoView {
view! { view! {
<div <div class="flex flex-col items-center justify-center" use:animate=pop_in_out()>
class="flex flex-col items-center justify-center" <div class="text-3xl font-bold mb-2">"Chromebook Loaner Form"</div>
use:animate=pop_in_out() <div class="text-lg">"Select an option from the left to begin."</div>
>
<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> </div>
} }
} }
@@ -431,34 +420,47 @@ pub fn LoanerReturnForm() -> impl IntoView {
<dialog <dialog
use:animate=pop_in_out() use:animate=pop_in_out()
class="p-3 m-auto rounded-md shadow-lg/33 backdrop:backdrop-blur-xs backdrop:transition-backdrop-filter" 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> <h2 class="text-2xl font-bold">"Confirmation"</h2>
<h4 class="text-xl mb-3"> <h4 class="text-xl mb-3">
{move || chosen_student.with(|opt_student| { {move || {
chosen_student
.with(|opt_student| {
if let Some(student) = opt_student.as_ref() { if let Some(student) = opt_student.as_ref() {
format!("Are you sure want to check in {} {}?", student.first_name, student.last_name) format!(
"Are you sure want to check in {} {}?",
student.first_name,
student.last_name,
)
} else { } else {
"There is no student selected.".to_string() "There is no student selected.".to_string()
} }
})} })
}}
</h4> </h4>
<ActionButton on:click=close_dialog disabled=dialog_disabled>"Close"</ActionButton> <ActionButton on:click=close_dialog disabled=dialog_disabled>
<ActionButton on:click=dialog_check_in disabled=dialog_disabled>"Check In"</ActionButton> "Close"
</ActionButton>
<ActionButton on:click=dialog_check_in disabled=dialog_disabled>
"Check In"
</ActionButton>
</dialog> </dialog>
<div <div class="flex flex-col gap-2 m-2" use:animate=pop_in_out()>
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"> <div class="flex flex-col justify-center gap-2 mb-3 py-3 px-2">
<h2 class="text-2xl font-bold">"Student List"</h2> <h2 class="text-2xl font-bold">"Student List"</h2>
<p class="text-lg">"Please select your name from the list."</p> <p class="text-lg">"Please select your name from the list."</p>
</div> </div>
<Suspense fallback=move || "Loading...".into_view()> <Suspense fallback=move || {
"Loading...".into_view()
}>
{move || { {move || {
match return_list_resource.get() { match return_list_resource.get() {
Some(loaner_list) => { Some(loaner_list) => {
let list_view = loaner_list.iter().map(|student| { let list_view = loaner_list
.iter()
.map(|student| {
let on_click = { let on_click = {
let student_click = student.clone(); let student_click = student.clone();
move |_| { move |_| {
@@ -469,31 +471,34 @@ pub fn LoanerReturnForm() -> impl IntoView {
} }
} }
}; };
let naive_time = chrono::NaiveDateTime::parse_from_str(
let naive_time = chrono::NaiveDateTime::parse_from_str(&student.date, "%H:%M, %m/%d/%Y") &student.date,
"%H:%M, %m/%d/%Y",
)
.unwrap(); .unwrap();
let time_zone = FixedOffset::west_opt(5 * 3600).unwrap(); let time_zone = FixedOffset::west_opt(5 * 3600).unwrap();
let student_date = time_zone.from_utc_datetime(&naive_time) let student_date = time_zone
.from_utc_datetime(&naive_time)
.format("%l:%M %p, %m/%d/%Y") .format("%l:%M %p, %m/%d/%Y")
.to_string(); .to_string();
log!("Found current date {:?}", student_date); log!("Found current date {:?}", student_date);
view! { 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 flex-row p-3 rounded-md cursor-pointer transition-shadow shadow-sm hover:shadow-lg/33"
on:click=on_click
>
<div class="flex-1"> <div class="flex-1">
{student.first_name.clone()}" "{student.last_name.clone()} {student.first_name.clone()}" "{student.last_name.clone()}
</div> </div>
<div class="flex-none"> <div class="flex-none">{student_date.clone()}</div>
{student_date.clone()}
</div>
</div> </div>
} }
}).collect_view(); })
.collect_view();
Either::Left(list_view) Either::Left(list_view)
}, }
None => Either::Right("No loaners found.".into_view()) None => Either::Right("No loaners found.".into_view()),
} }
}} }}
</Suspense> </Suspense>
+3 -2
View File
@@ -7,10 +7,11 @@ mod provider_portal;
mod scholarship_info; mod scholarship_info;
mod test_page; mod test_page;
mod unauthenticated_page; mod unauthenticated_page;
mod utils;
mod provider_contact; mod provider_contact;
mod auth_callback;
pub use self::{ pub use self::{
about_page::*, comparison_test::*, home_page::*, loaner_page::*, provider_portal::*, 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::*,
}; };
+5 -6
View File
@@ -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::logging::log;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::{AuthLoaded, Authenticated}; use leptos_oidc::{AuthLoaded, Authenticated};
use crate::common::ValueType; use crate::common::ValueType;
use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm}; use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
use crate::pages::UnauthenticatedPage; use crate::pages::UnauthenticatedPage;
use crate::pages::utils::get_user_claims; use crate::utils::get_user_claims;
#[cfg(feature = "ssr")]
use crate::pages::utils::server_utils::create_dynamo_client;
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts"; static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
@@ -52,7 +52,6 @@ async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>,
#[server] #[server]
async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> { async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> {
use aws_sdk_dynamodb::error::ProvideErrorMetadata; use aws_sdk_dynamodb::error::ProvideErrorMetadata;
use crate::pages::utils::server_utils::into_attr_map;
let client = create_dynamo_client().await; let client = create_dynamo_client().await;
+31 -2
View File
@@ -1,14 +1,32 @@
use crate::components::{Banner, DashboardButton}; use crate::components::{Banner, DashboardButton, DateList, Loading};
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::{AuthLoaded, Authenticated};
use crate::pages::api::get_important_dates;
use crate::pages::UnauthenticatedPage;
#[component] #[component]
pub fn ProviderPortal() -> impl IntoView { 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! { view! {
<Banner <Banner
title="Provider Dashboard" title="Provider Dashboard"
logo="PHS_Stacked_Acronym.png" logo="PHS_Stacked_Acronym.png"
path="/" path="/"
/> />
<AuthLoaded fallback=Loading>
<Authenticated unauthenticated=UnauthenticatedPage>
<div class="mx-auto px-6"> <div class="mx-auto px-6">
<div class="container mx-auto px-6 py-8 space-y-8"> <div class="container mx-auto px-6 py-8 space-y-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
@@ -33,10 +51,21 @@ pub fn ProviderPortal() -> impl IntoView {
/> />
</section> </section>
<section class="space-y-4"> <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> </section>
</div> </div>
</div> </div>
</div> </div>
</Authenticated>
</AuthLoaded>
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@ use leptos_router::hooks::{use_navigate, use_params};
use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType}; use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm}; use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
use super::UnauthenticatedPage; 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}; use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized, CreateTestComparisons};
@@ -609,7 +609,7 @@ fn ScholarshipForm(
items=vec![ 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.", "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?", "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() .iter()
.map(|prompt| prompt.to_string()) .map(|prompt| prompt.to_string())
+28 -11
View File
@@ -4,25 +4,42 @@ use leptos::prelude::*;
#[component] #[component]
pub fn TestPage() -> impl IntoView { pub fn TestPage() -> impl IntoView {
view! { view! {
<div class="p-6 space-y-4 max-w-1/3"> <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 <DashboardButton
title="Home Page" title="Profile"
description="Navigate to Home Page" description="Edit user profile"
icon="Book.png" icon="/Person_Black.png"
path="/" path="/"
/> />
<DashboardButton <DashboardButton
title="Home Page" title="Create Scholarship"
description="Navigate to Home Page" description="Navigate to Scholarship Creation Page"
icon="Calendar.png" icon="/Create_Black.png"
path="/" path="/"
/> />
<DashboardButton <DashboardButton
title="Home Page" title="Applicants"
description="Navigate to Home Page" description="View Scholarship Applicants"
icon="Edit.png" icon="/Form_Black.png"
path="/" path="/"
/> />
</section>
<section class="space-y-2">
<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>
} <div class="p-2">
<div class="flex flex-col p-3 gap-3">
</div>
</div>
</div>
</section>
</div>
</div>
</div>
}.into_any()
} }
+24 -16
View File
@@ -2,35 +2,43 @@ use crate::components::ActionButton;
use leptos::logging::log; use leptos::logging::log;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::AuthSignal; use leptos_oidc::AuthSignal;
use leptos_router::hooks::{use_navigate, use_location};
#[component] #[component]
pub fn UnauthenticatedPage() -> impl IntoView { pub fn UnauthenticatedPage() -> impl IntoView {
let auth = use_context::<AuthSignal>().expect("AuthSignal not present in LoginLink"); 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.with(|auth| {
auth.unauthenticated() auth.unauthenticated()
.map(|unauthenticated| unauthenticated.login_url()) .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", &current_path);
}
if let Some(url) = login_url.get() {
navigate(&url, Default::default())
}
}; };
Effect::new(move || log!("Generated login url: {:?}", login_url())); view! {
let navigate = leptos_router::hooks::use_navigate();
view! (
<div class="flex h-screen justify-center items-center pb-50"> <div class="flex h-screen justify-center items-center pb-50">
<div class="bg-white p-8 border rounded-lg shadow-lg"> <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"> <div class="flex pt-4 justify-center items-center">
<ActionButton on:click=move |_| { <ActionButton on:click=on_click>"Sign in"</ActionButton>
if let Some(url) = login_url() { </div>
navigate(&url, Default::default()) </div>
</div>
} }
}>
"Sign in"
</ActionButton>
</div>
</div>
</div>
)
} }
-42
View File
@@ -1,42 +0,0 @@
use leptos::prelude::{use_context, Signal, With};
use leptos_oidc::{Algorithm, AuthSignal, TokenData};
use crate::common::UserClaims;
/// # Server Utilities
///
/// These utility functions can only be used from the server. They are created to use server-side
/// dependencies like the AWS SDK. Use of these functions will crash the front end, as these
/// dependencies cannot be compiled for WASM (nor should they be).
#[cfg(feature = "ssr")]
pub mod server_utils {
use std::collections::HashMap;
use aws_config::SdkConfig;
use crate::common::ValueType;
/// Creates an [`SdkConfig`] struct for use with AWS SDK structs.
pub async fn create_aws_config() -> SdkConfig {
aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await
}
/// Creates a DynamoDB client.
pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client {
aws_sdk_dynamodb::Client::new(&create_aws_config().await)
}
pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> {
map.into_iter().map(|(k, v)| (k, v.into())).collect()
}
}
/// Gets the current user claims. This function should only be used in an area that has
/// access to an AuthSignal, or it will result in a total failure.
pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> {
let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal.");
Signal::derive(move || {
auth.with(|auth| {
auth.authenticated().and_then(|data| {
data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"])
})
})
})
}
+33
View File
@@ -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"])
})
})
})
}
+5
View File
@@ -0,0 +1,5 @@
#[cfg(feature = "ssr")]
pub mod server;
mod common;
pub use common::*;
+17
View File
@@ -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()
}