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:
37 files changed
+762
-274
No files matched your search
+32
-48
@@ -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]
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
use chrono::{DateTime, FixedOffset, Local};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DateInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub date: DateRange,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateInfo {
|
||||||
|
pub fn get_start_date(&self) -> DateTime<FixedOffset> {
|
||||||
|
match self.date {
|
||||||
|
DateRange::Single(date) | DateRange::Range(date, _) => date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum DateRange {
|
||||||
|
Single(DateTime<FixedOffset>),
|
||||||
|
Range(DateTime<FixedOffset>, DateTime<FixedOffset>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateRange {
|
||||||
|
pub fn get_status(&self) -> DateStatus {
|
||||||
|
match self {
|
||||||
|
DateRange::Single(_) => DateStatus::Blank,
|
||||||
|
DateRange::Range(start_date, end_date) => {
|
||||||
|
let local = Local::now();
|
||||||
|
// Check the amount of time from now to the start date. Values that are negative
|
||||||
|
// indicate that the start date has already passed.
|
||||||
|
if dbg!(start_date.signed_duration_since(local).num_days()) > 0 {
|
||||||
|
DateStatus::Upcoming
|
||||||
|
} else {
|
||||||
|
// Check the amount of time from now to the end date. The value will be positive if
|
||||||
|
// the end date is in the future. We will be specific to the minute.
|
||||||
|
let duration_until = dbg!(end_date).signed_duration_since(local);
|
||||||
|
let num_days = dbg!(duration_until.num_days());
|
||||||
|
let num_mins = dbg!(duration_until.num_minutes());
|
||||||
|
|
||||||
|
if num_mins <= 0 {
|
||||||
|
DateStatus::Closed
|
||||||
|
} else if num_days <= 5 {
|
||||||
|
DateStatus::Deadline
|
||||||
|
} else {
|
||||||
|
DateStatus::Open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
|
||||||
|
pub enum DateStatus {
|
||||||
|
Closed,
|
||||||
|
Deadline,
|
||||||
|
Open,
|
||||||
|
Upcoming,
|
||||||
|
Blank,
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ use std::collections::HashMap;
|
|||||||
/// now, the table will simply create a single massive `Map` field that contains all the stored
|
/// 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
@@ -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::*,
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,5 +57,5 @@ pub fn DashboardButton(
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}.into_any()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
use crate::common::{DateInfo, DateRange, DateStatus};
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
fn render_dates(dates: DateRange) -> impl IntoView {
|
||||||
|
let date_text = match dates {
|
||||||
|
DateRange::Single(date) => format!("{}", date.format("%A %B %d, %I:%M%p")),
|
||||||
|
DateRange::Range(start_date, end_date) =>
|
||||||
|
format!("{} - {}",
|
||||||
|
start_date.format("%D %I:%M%p"),
|
||||||
|
end_date.format("%D %I:%M%p")
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
view! { <span>{move || date_text.clone()}</span> }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_status(date: DateRange) -> impl IntoView {
|
||||||
|
let status = date.get_status();
|
||||||
|
|
||||||
|
let status_text = match dbg!(status.clone()) {
|
||||||
|
DateStatus::Blank => "",
|
||||||
|
DateStatus::Upcoming => "Upcoming",
|
||||||
|
DateStatus::Open => "Open",
|
||||||
|
DateStatus::Deadline => "Deadline",
|
||||||
|
DateStatus::Closed => "Closed",
|
||||||
|
};
|
||||||
|
|
||||||
|
let background_class = match status {
|
||||||
|
DateStatus::Blank => "hidden border-none",
|
||||||
|
DateStatus::Upcoming => "bg-yellow-100 text-yellow-800 border-yellow-100",
|
||||||
|
DateStatus::Open => "bg-green-100 text-green-800 border-green-100",
|
||||||
|
DateStatus::Deadline => "bg-red-100 text-red-800 border-red-100",
|
||||||
|
DateStatus::Closed => "bg-red-700 text-white border-red-700"
|
||||||
|
};
|
||||||
|
|
||||||
|
let classes =
|
||||||
|
format!("inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs
|
||||||
|
font-medium w-fit whitespace-nowrap shrink-0 {}",
|
||||||
|
background_class
|
||||||
|
);
|
||||||
|
|
||||||
|
view! { <span class=classes>{move || status_text}</span> }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Date Component
|
||||||
|
/// A single date, for use in a [`DateList`] component.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```
|
||||||
|
/// let date_info = DateInfo {
|
||||||
|
/// title: "Example Title".to_string(),
|
||||||
|
/// date: DateRange::Single("January 1st, 1990".to_string()),
|
||||||
|
/// description: "A description of the date.".to_string(),
|
||||||
|
/// status: DateStatus::Upcoming
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// view! {
|
||||||
|
/// <Date
|
||||||
|
/// info=date_info
|
||||||
|
/// />
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[component]
|
||||||
|
fn Date(
|
||||||
|
/// Struct of title, date, and description
|
||||||
|
#[prop(into)] info: Signal<DateInfo>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="flex items-start gap-3 rounded-lg border-grey-300 p-3
|
||||||
|
w-full text-left shadow-lg">
|
||||||
|
// <div class="flex-shrink-0 mt-0.5">
|
||||||
|
// <img src={icon} class="h-4 w-4" alt="icon"/>
|
||||||
|
// </div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="font-bold text-md text-base">{info.get().title}</h3>
|
||||||
|
<h3 class="text-md text-base mt-1">
|
||||||
|
{move || render_dates(info.get().date)}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600">{info.get().description}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 ml-2">{move || render_status(info.get().date)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///# Dates List Component
|
||||||
|
/// Displays a list of important dates in a column, with badges that draw attention to dates that
|
||||||
|
/// are more important.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```
|
||||||
|
/// let dates = vec![
|
||||||
|
/// DateInfo {
|
||||||
|
/// title: "Example Title".to_string(),
|
||||||
|
/// date: DateRange::Single("January 1st, 1990".to_string()),
|
||||||
|
/// description: "A description of the date.".to_string(),
|
||||||
|
/// status: DateStatus::Upcoming
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
///
|
||||||
|
/// view! {
|
||||||
|
/// <DateList dates=dates />
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[component]
|
||||||
|
pub fn DateList(
|
||||||
|
#[prop(into)] dates: Signal<Vec<DateInfo>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
{move || {
|
||||||
|
dates
|
||||||
|
.get()
|
||||||
|
.iter()
|
||||||
|
.map(|info| {
|
||||||
|
view! { <Date info=info.clone() /> }
|
||||||
|
})
|
||||||
|
.collect_view()
|
||||||
|
}}
|
||||||
|
}.into_any()
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mod provider;
|
||||||
|
pub use provider::*;
|
||||||
|
mod student;
|
||||||
|
pub use student::*;
|
||||||
|
pub(crate) mod setup;
|
||||||
@@ -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 />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_oidc::{Auth, AuthParameters, Challenge};
|
||||||
|
|
||||||
|
pub fn provide_auth_context(
|
||||||
|
origin: String,
|
||||||
|
issuer: impl Into<String>,
|
||||||
|
client_id: impl Into<String>,
|
||||||
|
redirect_path: impl Into<String>,
|
||||||
|
) {
|
||||||
|
let params = AuthParameters {
|
||||||
|
issuer: issuer.into(),
|
||||||
|
client_id: client_id.into(),
|
||||||
|
redirect_uri: format!("{}{}", origin, redirect_path.into()),
|
||||||
|
post_logout_redirect_uri: origin,
|
||||||
|
scope: Some("openid%20profile%20email".into()),
|
||||||
|
audience: None,
|
||||||
|
challenge: Challenge::None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let auth = Auth::signal();
|
||||||
|
provide_context(auth);
|
||||||
|
|
||||||
|
let _ = Auth::init(params);
|
||||||
|
}
|
||||||
@@ -0,0 +1,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 />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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::*
|
||||||
};
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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::*};
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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::*,
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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", ¤t_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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
use leptos::prelude::{use_context, Signal, With};
|
|
||||||
use leptos_oidc::{Algorithm, AuthSignal, TokenData};
|
|
||||||
use crate::common::UserClaims;
|
|
||||||
|
|
||||||
/// # Server Utilities
|
|
||||||
///
|
|
||||||
/// These utility functions can only be used from the server. They are created to use server-side
|
|
||||||
/// dependencies like the AWS SDK. Use of these functions will crash the front end, as these
|
|
||||||
/// dependencies cannot be compiled for WASM (nor should they be).
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub mod server_utils {
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use aws_config::SdkConfig;
|
|
||||||
use crate::common::ValueType;
|
|
||||||
|
|
||||||
/// Creates an [`SdkConfig`] struct for use with AWS SDK structs.
|
|
||||||
pub async fn create_aws_config() -> SdkConfig {
|
|
||||||
aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a DynamoDB client.
|
|
||||||
pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client {
|
|
||||||
aws_sdk_dynamodb::Client::new(&create_aws_config().await)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> {
|
|
||||||
map.into_iter().map(|(k, v)| (k, v.into())).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the current user claims. This function should only be used in an area that has
|
|
||||||
/// access to an AuthSignal, or it will result in a total failure.
|
|
||||||
pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> {
|
|
||||||
let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal.");
|
|
||||||
Signal::derive(move || {
|
|
||||||
auth.with(|auth| {
|
|
||||||
auth.authenticated().and_then(|data| {
|
|
||||||
data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_oidc::{Algorithm, AuthSignal, TokenData};
|
||||||
|
use crate::common::UserClaims;
|
||||||
|
|
||||||
|
pub fn use_origin() -> String {
|
||||||
|
#[cfg(target_arch = "wasm32")] {
|
||||||
|
use leptos::web_sys;
|
||||||
|
// Just get the current URL origin.
|
||||||
|
web_sys::window()
|
||||||
|
.unwrap()
|
||||||
|
.location()
|
||||||
|
.origin()
|
||||||
|
.unwrap_or("http://localhost:3000/".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))] {
|
||||||
|
// Read the expected origin out of an environment variable.
|
||||||
|
std::env::var("LP_SITE_ORIGIN").unwrap_or("http://localhost:3000/".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current user claims. This function should only be used in an area that has
|
||||||
|
/// access to an AuthSignal, or it will result in a total failure.
|
||||||
|
pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> {
|
||||||
|
let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal.");
|
||||||
|
Signal::derive(move || {
|
||||||
|
auth.with(|auth| {
|
||||||
|
auth.authenticated().and_then(|data| {
|
||||||
|
data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod server;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
pub use common::*;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use aws_config::SdkConfig;
|
||||||
|
use crate::common::ValueType;
|
||||||
|
|
||||||
|
/// Creates an [`SdkConfig`] struct for use with AWS SDK structs.
|
||||||
|
pub async fn create_aws_config() -> SdkConfig {
|
||||||
|
aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a DynamoDB client.
|
||||||
|
pub async fn create_dynamo_client() -> aws_sdk_dynamodb::Client {
|
||||||
|
aws_sdk_dynamodb::Client::new(&create_aws_config().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_attr_map(map: HashMap<String, ValueType>) -> HashMap<String, aws_sdk_dynamodb::types::AttributeValue> {
|
||||||
|
map.into_iter().map(|(k, v)| (k, v.into())).collect()
|
||||||
|
}
|
||||||
Reference in new issue
Block a user