Merge pull request #58 from R15-Software-Dev/feat/admin-page
PR with too many features
This commit was merged in pull request #58.
This commit is contained in:
56 files changed
+2205
-396
No files matched your search
+51
-1
@@ -1,4 +1,5 @@
|
|||||||
use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback};
|
use crate::pages::{ProviderPortal, ScholarshipInfoPage, ProviderContactPage, LoanerShell, LoanerFallback, LoanerBorrowForm, LoanerReturnForm, AuthCallbackPage, ApplicantsPageFallback, AdminShell, AdminHomePage, AdminProviderPage, AdminScholarshipPage, AdminUtilsPage};
|
||||||
|
use crate::pages::student;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
|
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
|
||||||
use leptos_oidc::AuthSignal;
|
use leptos_oidc::AuthSignal;
|
||||||
@@ -61,8 +62,10 @@ pub fn AppWithRoutes() -> impl IntoView {
|
|||||||
<ToastList>
|
<ToastList>
|
||||||
// TODO Create a 404 page
|
// TODO Create a 404 page
|
||||||
<Routes fallback=|| "Page not found.".into_view()>
|
<Routes fallback=|| "Page not found.".into_view()>
|
||||||
|
// <Route path=path!("/testing") view=TestPage />
|
||||||
<ProviderRoutes />
|
<ProviderRoutes />
|
||||||
<LoanerRoutes />
|
<LoanerRoutes />
|
||||||
|
<StudentRoutes />
|
||||||
</Routes>
|
</Routes>
|
||||||
</ToastList>
|
</ToastList>
|
||||||
</main>
|
</main>
|
||||||
@@ -100,6 +103,53 @@ fn LoanerRoutes() -> impl MatchNestedRoutes + Clone {
|
|||||||
.into_any_nested_route()
|
.into_any_nested_route()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component(transparent)]
|
||||||
|
fn AdminRoutes() -> impl MatchNestedRoutes + Clone {
|
||||||
|
view! {
|
||||||
|
<ParentRoute path=path!("admin") view=AdminShell>
|
||||||
|
<Route path=path!("home") view=AdminHomePage />
|
||||||
|
<Route path=path!("providers") view=AdminProviderPage />
|
||||||
|
<Route path=path!("scholarships") view=AdminScholarshipPage />
|
||||||
|
<Route path=path!("utilities") view=AdminUtilsPage />
|
||||||
|
</ParentRoute>
|
||||||
|
}
|
||||||
|
.into_inner()
|
||||||
|
.into_any_nested_route()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component(transparent)]
|
||||||
|
fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
|
||||||
|
view! {
|
||||||
|
<ParentRoute path=path!("students") view=student::StudentShell>
|
||||||
|
<Route path=path!("callback") view=AuthCallbackPage />
|
||||||
|
<Route path=path!("home") view=student::StudentHomePage />
|
||||||
|
<Route path=path!("demographics") view=student::StudentDemographicsPage />
|
||||||
|
<Route path=path!("eligibility") view=student::StudentEligibilityPage />
|
||||||
|
<Route path=path!("additional") view=student::AdditionalPage />
|
||||||
|
<Route path=path!("additional/academics") view=student::StudentAcademicsPage />
|
||||||
|
<Route path=path!("additional/athletics") view=student::StudentAthleticsPage />
|
||||||
|
<Route
|
||||||
|
path=path!("additional/extracurriculars")
|
||||||
|
view=student::StudentExtracurricularsPage
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path=path!("additional/work-experience")
|
||||||
|
view=student::StudentWorkExperiencePage
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path=path!("additional/university")
|
||||||
|
view=student::StudentUniversityPage
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path=path!("additional/family-info")
|
||||||
|
view=student::StudentFamilyPage
|
||||||
|
/>
|
||||||
|
</ParentRoute>
|
||||||
|
}
|
||||||
|
.into_inner()
|
||||||
|
.into_any_nested_route()
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AuthErrorView() -> impl IntoView {
|
pub fn AuthErrorView() -> impl IntoView {
|
||||||
let auth = use_context::<AuthSignal>().expect("AuthError: RwSignal<AuthStore> was not found");
|
let auth = use_context::<AuthSignal>().expect("AuthError: RwSignal<AuthStore> was not found");
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use super::{
|
|||||||
Comparison, MapListComparison, NestedListComparison, NumberComparison, NumberListComparison,
|
Comparison, MapListComparison, NestedListComparison, NumberComparison, NumberListComparison,
|
||||||
TextComparison, TextListComparison,
|
TextComparison, TextListComparison,
|
||||||
};
|
};
|
||||||
use crate::common::{ExpandableInfo, ValueType};
|
use crate::common::ValueType;
|
||||||
|
use std::collections::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
@@ -42,15 +43,13 @@ impl ComparisonData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compare(&self, student_data: &ExpandableInfo) -> Result<bool, String> {
|
pub fn compare(&self, student_data: &HashMap<String, ValueType>) -> Result<bool, String> {
|
||||||
let value = student_data.data.get(&self.member).expect(
|
student_data.get(&self.member)
|
||||||
format!(
|
.ok_or_else(||
|
||||||
"Couldn't find member {:?}, current student data: {:?}",
|
format!("Couldn't find member {:?}, current student data: {:?}", self.member, student_data)
|
||||||
self.member, student_data
|
|
||||||
)
|
)
|
||||||
.as_str(),
|
.and_then(|value| self.comparison.evaluate(value, &self.target_value))
|
||||||
);
|
.or_else(|err| Err(err.to_string()))
|
||||||
self.comparison.evaluate(value, &self.target_value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +105,11 @@ mod tests {
|
|||||||
display_text: "First Name is Jane".to_string(),
|
display_text: "First Name is Jane".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result = text_comp_success.compare(&student_data);
|
let mut result = text_comp_success.compare(&student_data.data);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(result.unwrap(), true);
|
assert_eq!(result.unwrap(), true);
|
||||||
|
|
||||||
result = text_comp_fail.compare(&student_data);
|
result = text_comp_fail.compare(&student_data.data);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,14 +17,16 @@ impl Comparison for NumberComparison {
|
|||||||
// The prerequisites for all number comparisons is that the target value is Some(number) and the
|
// The prerequisites for all number comparisons is that the target value is Some(number) and the
|
||||||
// comparison member points to Some(number) in the student_info.
|
// comparison member points to Some(number) in the student_info.
|
||||||
let student_value = match comp_value {
|
let student_value = match comp_value {
|
||||||
ValueType::Number(Some(num)) => num.parse::<i32>().unwrap(),
|
ValueType::Number(Some(num)) => num.parse::<f32>().unwrap(),
|
||||||
_ => {
|
_ => {
|
||||||
return Err("Student value is not a number".into());
|
return Err("Student value is not a number".into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
leptos::logging::log!("Parsing value: {:?}", target_value);
|
||||||
|
|
||||||
let target = match target_value {
|
let target = match target_value {
|
||||||
ValueType::Number(Some(num)) => num.parse::<i32>().unwrap(),
|
ValueType::Number(Some(num)) => num.parse::<f32>().unwrap(),
|
||||||
_ => {
|
_ => {
|
||||||
return Err("Target value is not a number".into());
|
return Err("Target value is not a number".into());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,14 @@ pub enum InputType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InputType {
|
impl InputType {
|
||||||
pub fn into_view(self, data_map: RwSignal<HashMap<String, ValueType>>) -> impl IntoView {
|
/// Converts an `InputType` into an `impl IntoView`. A name must be specified in the case that
|
||||||
|
/// the component is a checkbox/radio list, which requires a unique name to function properly.
|
||||||
|
/// Many times, the name will be the same as the data member.
|
||||||
|
pub fn into_view(
|
||||||
|
self,
|
||||||
|
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
|
name: String,
|
||||||
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
{match self {
|
{match self {
|
||||||
InputType::Text(member, label, placeholder, required) =>
|
InputType::Text(member, label, placeholder, required) =>
|
||||||
@@ -56,6 +63,7 @@ impl InputType {
|
|||||||
view! {
|
view! {
|
||||||
<CheckboxList
|
<CheckboxList
|
||||||
label = label.clone()
|
label = label.clone()
|
||||||
|
name = name.clone()
|
||||||
items = options.clone()
|
items = options.clone()
|
||||||
data_member = member.clone()
|
data_member = member.clone()
|
||||||
data_map = data_map
|
data_map = data_map
|
||||||
|
|||||||
+2
-1
@@ -7,8 +7,9 @@ mod submit_status;
|
|||||||
mod user;
|
mod user;
|
||||||
mod value_type;
|
mod value_type;
|
||||||
pub mod animations;
|
pub mod animations;
|
||||||
|
mod tab_info;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*,
|
comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*,
|
||||||
submit_status::*, user::*, value_type::*,
|
submit_status::*, user::*, value_type::*, tab_info::*,
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TabInfo {
|
||||||
|
pub text: String,
|
||||||
|
pub path: String,
|
||||||
|
pub sub_paths: Vec<SubTabInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabInfo {
|
||||||
|
pub fn new(
|
||||||
|
text: impl Into<String>,
|
||||||
|
path: impl Into<String>,
|
||||||
|
sub_paths: Option<Vec<SubTabInfo>>
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
path: path.into(),
|
||||||
|
sub_paths: sub_paths.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SubTabInfo {
|
||||||
|
pub text: String,
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubTabInfo {
|
||||||
|
pub fn new(text: impl Into<String>, path: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
path: path.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-28
@@ -22,7 +22,7 @@ pub fn Banner(
|
|||||||
title: String,
|
title: String,
|
||||||
/// The file path or URL of a logo displayed at the top left of the banner.
|
/// The file path or URL of a logo displayed at the top left of the banner.
|
||||||
#[prop(into)]
|
#[prop(into)]
|
||||||
logo: String,
|
logo: Signal<String>,
|
||||||
/// The route that the navigates user to a page (home page).
|
/// The route that the navigates user to a page (home page).
|
||||||
#[prop(into, optional)]
|
#[prop(into, optional)]
|
||||||
path: Signal<String>,
|
path: Signal<String>,
|
||||||
@@ -39,36 +39,26 @@ pub fn Banner(
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="mx-auto">
|
<div class="w-screen bg-red-900">
|
||||||
<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 flex-row items-center gap-4 p-6 pb-2">
|
||||||
<div class="flex flex-row items-center gap-4 p-6 pb-2">
|
<div class="flex-shrink-0">
|
||||||
<div class="flex-shrink-0">
|
<img src=logo 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 class="flex-1">
|
|
||||||
<h2 class="text-white text-2xl font-bold">{title.clone()}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex-1">
|
||||||
class="flex flex-row items-center gap-4"
|
<h2 class="text-white text-2xl font-bold">{title.clone()}</h2>
|
||||||
class=("p-2", move || !show_back.get())
|
|
||||||
>
|
|
||||||
<Show when=move || show_back.get()>
|
|
||||||
<a
|
|
||||||
href=path
|
|
||||||
class="flex flex-row items-center gap-2 m-2 ml-14 text-gray-200 hover:underline"
|
|
||||||
>
|
|
||||||
<Icon icon=icondata::FaArrowLeftSolid />
|
|
||||||
<span>"Back to Dashboard"</span>
|
|
||||||
</a>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-4" class=("p-2", move || !show_back.get())>
|
||||||
|
<Show when=move || show_back.get()>
|
||||||
|
<a
|
||||||
|
href=path
|
||||||
|
class="flex flex-row items-center gap-2 m-2 ml-14 text-gray-200 hover:underline"
|
||||||
|
>
|
||||||
|
<Icon icon=icondata::FaArrowLeftSolid />
|
||||||
|
<span>"Back to Dashboard"</span>
|
||||||
|
</a>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +103,10 @@ pub fn Checkbox(
|
|||||||
/// ```
|
/// ```
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CheckboxList(
|
pub fn CheckboxList(
|
||||||
|
/// The name that this list should use. This doesn't need to be specified all the time, but is
|
||||||
|
/// for use in elements that may treat this list as a sub-input (components like the [`MultiEntry`]
|
||||||
|
/// do this)
|
||||||
|
#[prop(optional, into)] name: Signal<String>,
|
||||||
/// The data member that this list should edit/display from the `data_map`.
|
/// The data member that this list should edit/display from the `data_map`.
|
||||||
#[prop(into)] data_member: Signal<String>,
|
#[prop(into)] data_member: Signal<String>,
|
||||||
/// A [`Signal`] allowing access to a data store.
|
/// A [`Signal`] allowing access to a data store.
|
||||||
@@ -160,7 +164,7 @@ pub fn CheckboxList(
|
|||||||
on_change=on_change
|
on_change=on_change
|
||||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||||
value=item
|
value=item
|
||||||
name=data_member
|
name=move || format!("{}_{}", name.get(), data_member.get())
|
||||||
disabled=disabled
|
disabled=disabled
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
mod checkbox;
|
mod checkbox;
|
||||||
mod chips;
|
mod chips;
|
||||||
|
mod select_nav_list;
|
||||||
|
|
||||||
pub use checkbox::*;
|
pub use checkbox::*;
|
||||||
pub use chips::*;
|
pub use chips::*;
|
||||||
|
pub use select_nav_list::*;
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
use leptos::logging::log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_router::components::A;
|
||||||
|
use crate::components::utils;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NavListInfo {
|
||||||
|
pub text: Vec<String>,
|
||||||
|
pub id: String,
|
||||||
|
pub slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn NavListItem(
|
||||||
|
#[prop(into)] name: Signal<String>,
|
||||||
|
#[prop(into)] value: Signal<String>,
|
||||||
|
#[prop(into)] text: Signal<Vec<String>>,
|
||||||
|
#[prop(into)] slug: Signal<String>,
|
||||||
|
#[prop(into)] selected: Signal<bool>,
|
||||||
|
#[prop(into)] on_change: Callback<()>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let linked_info = Signal::derive(move ||
|
||||||
|
text.get()
|
||||||
|
.get(0)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
);
|
||||||
|
let other_info = Signal::derive(move ||
|
||||||
|
text.get()
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.skip(1)
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="flex flex-row gap-2 p-1 rounded-lg"
|
||||||
|
class=(["bg-yellow-100"], move || selected.get())
|
||||||
|
>
|
||||||
|
<NavListCheckbox
|
||||||
|
name=name
|
||||||
|
value=value
|
||||||
|
on_change=move || on_change.run(())
|
||||||
|
selected=selected
|
||||||
|
/>
|
||||||
|
<A
|
||||||
|
href=move || slug.get()
|
||||||
|
{..}
|
||||||
|
class="flex flex-1 items-center underline hover:text-blue-400"
|
||||||
|
>
|
||||||
|
{linked_info}
|
||||||
|
</A>
|
||||||
|
<For each=move || other_info.get() key=|info| info.clone() let(info)>
|
||||||
|
<p class="flex flex-1 items-center">{info}</p>
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn NavListCheckbox(
|
||||||
|
#[prop(into)] selected: Signal<bool>,
|
||||||
|
#[prop(into)] on_change: Callback<()>,
|
||||||
|
#[prop(into)] name: Signal<String>,
|
||||||
|
#[prop(into)] value: Signal<String>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let id = Memo::new(move |_| utils::create_unique_id(&name.get(), &value.get()));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<label for=id class="flex items-center">
|
||||||
|
<input
|
||||||
|
class="hidden relative peer shrink-0"
|
||||||
|
type="checkbox"
|
||||||
|
name=name
|
||||||
|
id=id
|
||||||
|
prop:checked=selected
|
||||||
|
on:change=move |_| on_change.run(())
|
||||||
|
/>
|
||||||
|
<div class="relative inline-block h-5 w-5 m-2 rounded-sm border border-gray-300 bg-white
|
||||||
|
transition-bg duration-150 peer-checked:bg-red-700 peer-checked:border-red-700
|
||||||
|
[&>span]:opacity-0 peer-checked:[&>span]:opacity-100
|
||||||
|
peer-disabled:bg-gray-600/33 peer-disabled:border-gray-600
|
||||||
|
peer-disabled:peer-checked:bg-gray-600">
|
||||||
|
// This span is the check icon. It is centered within the div and has a border on the bottom
|
||||||
|
// and right sides, which is then rotated to appear like a checkmark.
|
||||||
|
<span class="absolute left-1/2 top-1/2 block h-3 w-1.5 -translate-x-1/2 -translate-y-1/2
|
||||||
|
rotate-45 transform border-b-3 border-r-3
|
||||||
|
border-white transition-opacity duration-150"></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Selectable Nav-List Component
|
||||||
|
///
|
||||||
|
/// This component takes a `Vec<NavListInfo>` to show a list. The list contains links that append
|
||||||
|
/// the `NavListItem::slug` to the current path, and each item is selectable. The list of selected
|
||||||
|
/// items is usable in this component's parent.
|
||||||
|
#[component]
|
||||||
|
pub fn SelectableNavList(
|
||||||
|
#[prop(into)] selected: RwSignal<Vec<String>>,
|
||||||
|
#[prop(into)] items: Signal<Vec<NavListInfo>>,
|
||||||
|
#[prop(into)] headers: Signal<Vec<String>>,
|
||||||
|
#[prop(into)] name: Signal<String>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let all_selected = RwSignal::new(false);
|
||||||
|
let all_selected_change = move || {
|
||||||
|
if all_selected.get() {
|
||||||
|
log!("Unchecking all values.");
|
||||||
|
all_selected.set(false);
|
||||||
|
selected.set(Vec::new());
|
||||||
|
} else {
|
||||||
|
log!("Checking all values.");
|
||||||
|
all_selected.set(true);
|
||||||
|
selected.set(
|
||||||
|
items
|
||||||
|
.get()
|
||||||
|
.iter()
|
||||||
|
.map(|info| info.id.clone())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-col m-3">
|
||||||
|
// Header block - contains evenly spaced headers and the "select all" checkbox.
|
||||||
|
// Include a checkbox here that's got the same style as the items. This maintains spacing.
|
||||||
|
// All headers *must* be evenly spaced, otherwise the list starts getting confusing.
|
||||||
|
<div class="flex flex-row bg-red-800 rounded-lg p-1 gap-2">
|
||||||
|
<NavListCheckbox
|
||||||
|
selected=all_selected
|
||||||
|
name=name
|
||||||
|
value="select-all"
|
||||||
|
on_change=all_selected_change
|
||||||
|
/>
|
||||||
|
<For each=move || headers.get() key=|header| header.clone() let(header_text)>
|
||||||
|
<div class="flex flex-1 text-white font-bold items-center">{header_text}</div>
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
// Items block - contains constructed items. These are selectable.
|
||||||
|
<For
|
||||||
|
each=move || items.get()
|
||||||
|
key=|item| item.id.clone()
|
||||||
|
children=move |entry| {
|
||||||
|
let id = Signal::derive(move || entry.id.clone());
|
||||||
|
let text = Signal::derive(move || entry.text.clone());
|
||||||
|
let selected_sig = Signal::derive(move || selected.get().contains(&id.get()));
|
||||||
|
let on_change = move || {
|
||||||
|
selected
|
||||||
|
.update(|list| {
|
||||||
|
let id = id.get();
|
||||||
|
if list.contains(&id) {
|
||||||
|
log!("Found value {}, removing.", id);
|
||||||
|
list.retain(|s| s != &id);
|
||||||
|
} else {
|
||||||
|
log!("Adding value {} to the list.", id);
|
||||||
|
list.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
all_selected.set(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<NavListItem
|
||||||
|
name=name
|
||||||
|
text=text
|
||||||
|
value=id.clone()
|
||||||
|
slug=entry.slug
|
||||||
|
selected=selected_sig
|
||||||
|
on_change=on_change
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,6 +151,7 @@ pub fn RadioList(
|
|||||||
#[prop(optional, into)] label: String,
|
#[prop(optional, into)] label: String,
|
||||||
#[prop(optional, into)] required: Signal<bool>,
|
#[prop(optional, into)] required: Signal<bool>,
|
||||||
#[prop(optional, into)] other_prompt: Signal<String>,
|
#[prop(optional, into)] other_prompt: Signal<String>,
|
||||||
|
#[prop(optional, into)] name: Signal<String>
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
//#region Value Logic
|
//#region Value Logic
|
||||||
|
|
||||||
@@ -262,7 +263,7 @@ pub fn RadioList(
|
|||||||
on_change=on_change
|
on_change=on_change
|
||||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||||
value=item
|
value=item
|
||||||
name=data_member
|
name=move || format!("{}_{}", name.get(), data_member.get())
|
||||||
disabled=disabled
|
disabled=disabled
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use leptos::ev::{Event, Targeted};
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::web_sys::HtmlSelectElement;
|
use leptos::web_sys::HtmlSelectElement;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use leptos::logging::{log, warn};
|
|
||||||
use crate::components::{use_validation_context, InputState, ValidationState};
|
use crate::components::{use_validation_context, InputState, ValidationState};
|
||||||
|
|
||||||
fn validate(required: bool, value: String) -> ValidationState {
|
fn validate(required: bool, value: String) -> ValidationState {
|
||||||
@@ -20,7 +19,7 @@ fn validate(required: bool, value: String) -> ValidationState {
|
|||||||
/// by the `value` prop of the component.
|
/// by the `value` prop of the component.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Select(
|
pub fn Select(
|
||||||
#[prop()] value_list: Vec<String>,
|
#[prop(into)] value_list: Signal<Vec<String>>,
|
||||||
#[prop(into)] data_member: Signal<String>,
|
#[prop(into)] data_member: Signal<String>,
|
||||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
#[prop(optional, into)] label: String,
|
#[prop(optional, into)] label: String,
|
||||||
@@ -29,27 +28,18 @@ pub fn Select(
|
|||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
//#region Value Logic
|
//#region Value Logic
|
||||||
|
|
||||||
let default_raw_value = data_map.get_untracked()
|
let value = Memo::new(move |_| {
|
||||||
.get(&data_member.get_untracked())
|
data_map.with(|map| {
|
||||||
.cloned()
|
map.get(&data_member.get_untracked())
|
||||||
.unwrap_or_default()
|
.and_then(|v| v.as_string().ok().flatten())
|
||||||
.as_string().unwrap_or_default().unwrap_or_default();
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
let raw_value = RwSignal::new(default_raw_value);
|
|
||||||
|
|
||||||
Effect::new(move || {
|
|
||||||
data_map.update(|map| {
|
|
||||||
map.insert(data_member.get(), ValueType::String(Some(raw_value.get())));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Validation Logic
|
//#region Validation Logic
|
||||||
|
|
||||||
let error = RwSignal::new(
|
let error = Memo::new(move |_| validate(required.get(), value.get()));
|
||||||
validate(required.get_untracked(), raw_value.get_untracked())
|
|
||||||
);
|
|
||||||
warn!("Current error state: {:?}", error.get_untracked());
|
|
||||||
let dirty = RwSignal::new(false);
|
let dirty = RwSignal::new(false);
|
||||||
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
|
let show_errors = Signal::derive(move || dirty.get() && matches!(error.get(), ValidationState::Invalid(_)));
|
||||||
|
|
||||||
@@ -71,16 +61,9 @@ pub fn Select(
|
|||||||
|
|
||||||
let on_change = move |e: Targeted<Event, HtmlSelectElement>| {
|
let on_change = move |e: Targeted<Event, HtmlSelectElement>| {
|
||||||
let value = e.target().value();
|
let value = e.target().value();
|
||||||
log!("Found value {}", value);
|
|
||||||
raw_value.set(value.clone());
|
|
||||||
error.set(validate(required.get(), raw_value.get()));
|
|
||||||
dirty.set(true);
|
dirty.set(true);
|
||||||
data_map.update(|map| {
|
data_map.update(|map| {
|
||||||
if let Some(val) = map.get_mut(&data_member.get()) {
|
map.insert(data_member.get(), ValueType::String(Some(value)));
|
||||||
*val = ValueType::String(Some(value.clone()));
|
|
||||||
} else {
|
|
||||||
map.insert(data_member.get(), ValueType::String(Some(value)));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,27 +79,21 @@ pub fn Select(
|
|||||||
transition-border duration-150
|
transition-border 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"
|
||||||
|
prop:value=move || value.get()
|
||||||
on:change:target=on_change
|
on:change:target=on_change
|
||||||
disabled=disabled
|
disabled=disabled
|
||||||
>
|
>
|
||||||
// This closure handles the display of the options.
|
// This closure handles the display of the options.
|
||||||
<option
|
<option value="" disabled hidden selected=move || value.get().is_empty()>
|
||||||
value=""
|
|
||||||
disabled
|
|
||||||
hidden
|
|
||||||
selected=move || raw_value.get().is_empty()
|
|
||||||
>
|
|
||||||
"Select one..."
|
"Select one..."
|
||||||
</option>
|
</option>
|
||||||
{move || {
|
|
||||||
value_list
|
// We can reasonably assume that the options in the list will be distinct.
|
||||||
.iter()
|
<For
|
||||||
.map(move |value| {
|
each=move || value_list.get()
|
||||||
let display = value.to_owned();
|
key=|value| value.clone()
|
||||||
view! { <option value=display>{display.clone()}</option> }
|
children=move |val| view! { <option value=val>{val.clone()}</option> }
|
||||||
})
|
/>
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}}
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use leptos::prelude::*;
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn Loading() -> impl IntoView {
|
pub fn Loading() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<div class="size-20 mx-auto">
|
<div class="size-20 mx-auto mt-20">
|
||||||
<img src="/loading.gif" />
|
<img src="/loading.gif" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::login::setup::provide_auth_context;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdminLoginContext(
|
||||||
|
children: Children
|
||||||
|
) -> impl IntoView {
|
||||||
|
provide_auth_context(
|
||||||
|
crate::utils::use_origin(),
|
||||||
|
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
|
||||||
|
"56c2bqvl021rv8d5mq36blt7jv",
|
||||||
|
"/providers/callback"
|
||||||
|
);
|
||||||
|
|
||||||
|
view! { {children()} }
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
pub(crate) mod setup;
|
||||||
mod provider;
|
mod provider;
|
||||||
pub use provider::*;
|
pub use provider::*;
|
||||||
mod student;
|
mod student;
|
||||||
pub use student::*;
|
pub use student::*;
|
||||||
pub(crate) mod setup;
|
mod admin;
|
||||||
|
pub use admin::*;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_router::components::Outlet;
|
|
||||||
use super::setup::provide_auth_context;
|
use super::setup::provide_auth_context;
|
||||||
|
|
||||||
///# Student Login Context Component
|
///# Student Login Context Component
|
||||||
@@ -16,7 +15,9 @@ use super::setup::provide_auth_context;
|
|||||||
/// </StudentLoginContext>
|
/// </StudentLoginContext>
|
||||||
/// }
|
/// }
|
||||||
#[component]
|
#[component]
|
||||||
pub fn StudentLoginContext() -> impl IntoView {
|
pub fn StudentLoginContext(
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
provide_auth_context(
|
provide_auth_context(
|
||||||
crate::utils::use_origin(),
|
crate::utils::use_origin(),
|
||||||
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
|
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
|
||||||
@@ -24,5 +25,5 @@ pub fn StudentLoginContext() -> impl IntoView {
|
|||||||
"/students/callback"
|
"/students/callback"
|
||||||
);
|
);
|
||||||
|
|
||||||
view! { <Outlet /> }
|
view! { {children()} }
|
||||||
}
|
}
|
||||||
@@ -13,9 +13,10 @@ mod lists;
|
|||||||
mod toasts;
|
mod toasts;
|
||||||
mod header;
|
mod header;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
mod tabs;
|
||||||
|
|
||||||
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::*, date::*
|
validated_form::*, toasts::*, header::*, date::*, tabs::*
|
||||||
};
|
};
|
||||||
+112
-66
@@ -1,8 +1,9 @@
|
|||||||
use crate::common::{InputType, ValueType};
|
use crate::common::{InputType, ValueType};
|
||||||
use crate::components::ActionButton;
|
use crate::components::ActionButton;
|
||||||
use leptos::leptos_dom::logging::console_log;
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use leptos::logging::log;
|
||||||
|
use leptos_icons::Icon;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// region Helper Functions
|
// region Helper Functions
|
||||||
@@ -12,6 +13,14 @@ fn get_uuid_from_map(map: &HashMap<String, ValueType>) -> Option<String> {
|
|||||||
map.get("uuid")?.as_string().unwrap_or(None)
|
map.get("uuid")?.as_string().unwrap_or(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Does the same as [`get_uuid_from_map`], but starts from a [`ValueType`] enum.
|
||||||
|
fn get_uuid(vt: ValueType) -> Option<String> {
|
||||||
|
vt.as_map().ok().flatten().unwrap_or_default()
|
||||||
|
.get("uuid")?
|
||||||
|
.as_string()
|
||||||
|
.ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates an Entry within the given `Vec`.
|
/// Updates an Entry within the given `Vec`.
|
||||||
fn update_entry_list(list: &mut Vec<ValueType>, data: &HashMap<String, ValueType>) {
|
fn update_entry_list(list: &mut Vec<ValueType>, data: &HashMap<String, ValueType>) {
|
||||||
if let Some(uuid) = get_uuid_from_map(&data) {
|
if let Some(uuid) = get_uuid_from_map(&data) {
|
||||||
@@ -66,81 +75,103 @@ fn new_entry() -> ValueType {
|
|||||||
/// `first_name` and `last_name` keys within that `Entry`.
|
/// `first_name` and `last_name` keys within that `Entry`.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MultiEntry(
|
pub fn MultiEntry(
|
||||||
|
#[prop(into)] label: Signal<String>,
|
||||||
|
#[prop(optional, into)] description: Signal<String>,
|
||||||
/// The data member that contains the list of entries within the `data_map`.
|
/// The data member that contains the list of entries within the `data_map`.
|
||||||
#[prop(into)]
|
#[prop(into)]
|
||||||
data_member: String,
|
data_member: Signal<String>,
|
||||||
/// The data map that contains all the information. Data is found within this map using the
|
/// The data map that contains all the information. Data is found within this map using the
|
||||||
/// component's `data_member` input.
|
/// component's `data_member` input.
|
||||||
#[prop()]
|
#[prop()]
|
||||||
data_map: RwSignal<HashMap<String, ValueType>>,
|
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
/// The type and order of the inputs for each `Entry` component within this `MultiEntry` component.
|
/// The type and order of the inputs for each `Entry` component within this `MultiEntry` component.
|
||||||
#[prop(optional)]
|
#[prop(optional, into)]
|
||||||
schema: Vec<InputType>,
|
schema: Signal<Vec<InputType>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
// Make the values within the list reactive. Each of these values should be a ValueType::Map,
|
// Make the values within the list reactive. Each of these values should be a ValueType::Map,
|
||||||
// so we can pass these directly to the application.
|
// so we can pass these directly to the application.
|
||||||
let data_reactive = RwSignal::new(
|
let data_list = Memo::new(move |_| {
|
||||||
data_map
|
data_map.with(|map| {
|
||||||
.get_untracked()
|
map.get(&data_member.get())
|
||||||
.get(&data_member)
|
.and_then(|v| v.as_list().ok().flatten())
|
||||||
.unwrap_or(&ValueType::List(None))
|
.unwrap_or_default()
|
||||||
.as_list()
|
})
|
||||||
.unwrap_or(Some(Vec::<ValueType>::new()))
|
|
||||||
.unwrap_or(Vec::new()),
|
|
||||||
);
|
|
||||||
|
|
||||||
// This effect updates the data_map with any new values from the data_reactive list, every time
|
|
||||||
// the data_reactive list is changed. This allows changes from within the Entry component to
|
|
||||||
// bubble up to the data_map effectively.
|
|
||||||
Effect::new(move |_| {
|
|
||||||
let entry_list = data_reactive.get();
|
|
||||||
data_map.update(|map| {
|
|
||||||
map.insert(data_member.clone(), ValueType::List(Some(entry_list)));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let add_entry = move |_| data_reactive.update(|list| list.push(new_entry()));
|
// This effect updates the data_map with any new values from the data_list, every time
|
||||||
|
// the data_list is changed. This allows changes from within the Entry component to
|
||||||
|
// bubble up to the data_map effectively.
|
||||||
|
let update_parent_list = move |list| {
|
||||||
|
log!("Updating parent list using child list {:?}", list);
|
||||||
|
data_map.update(|map| {
|
||||||
|
map.insert(data_member.get(), ValueType::List(Some(list)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_entry = move |_| data_map.update(|map| {
|
||||||
|
log!("Adding new entry.");
|
||||||
|
let mut list = data_list.get_untracked();
|
||||||
|
|
||||||
|
list.push(new_entry());
|
||||||
|
map.insert(data_member.get(), ValueType::List(Some(list)));
|
||||||
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<div class="flex flex-col gap-2 p-2">
|
<div class="flex flex-col gap-2 p-2">
|
||||||
|
<span class="font-bold">{label}</span>
|
||||||
|
<Show when=move || !description.get().is_empty()>
|
||||||
|
<span>{description}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-6 p-2">
|
||||||
// Render the entries
|
// Render the entries
|
||||||
<For
|
<Show
|
||||||
each=move || data_reactive.get()
|
when=move || !data_list.get().is_empty()
|
||||||
key=|entry| get_uuid_from_map(&entry.as_map().unwrap().unwrap()) // Entries MUST be given a unique ID. This is absolutely ridiculous.
|
fallback=|| {
|
||||||
children=move |mut entry_map| {
|
view! { <div class="mx-auto">"You haven't added any entries yet."</div> }
|
||||||
// The map_signal is the map used for the internal element. This signal will
|
|
||||||
// keep track of the individual entry's data, and whenever it is changed,
|
|
||||||
// the parent data_map will also be updated to reflect these changes.
|
|
||||||
let map_signal = RwSignal::new(entry_map
|
|
||||||
.as_map()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.unwrap_or_default());
|
|
||||||
|
|
||||||
// This effect updates the data_reactive list with the map_signal values, every
|
|
||||||
// time the map_signal is changed.
|
|
||||||
Effect::new(move |_| {
|
|
||||||
let child_map = map_signal.get(); // HashMap
|
|
||||||
data_reactive.update(|mut list| {
|
|
||||||
update_entry_list(&mut list, &child_map);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<Entry
|
|
||||||
data_map = map_signal
|
|
||||||
schema = schema.clone()
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<For
|
||||||
|
each=move || data_list.get()
|
||||||
|
// Entries MUST be given a unique ID.
|
||||||
|
key=|entry| get_uuid(entry.clone())
|
||||||
|
children=move |mut entry_map| {
|
||||||
|
let map_signal = RwSignal::new(
|
||||||
|
entry_map.as_map().ok().flatten().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
let unique_id = Memo::new(move |_| {
|
||||||
|
get_uuid_from_map(&map_signal.get()).unwrap_or_default()
|
||||||
|
});
|
||||||
|
let on_delete = move || {
|
||||||
|
let mut list = data_list.get_untracked();
|
||||||
|
let uuid = unique_id.get_untracked();
|
||||||
|
list.retain(|map| {
|
||||||
|
get_uuid(map.clone()).unwrap_or_default() != uuid
|
||||||
|
});
|
||||||
|
update_parent_list(list);
|
||||||
|
};
|
||||||
|
Effect::new(move |_| {
|
||||||
|
let child_map = map_signal.get();
|
||||||
|
let mut list = data_list.get_untracked();
|
||||||
|
update_entry_list(&mut list, &child_map);
|
||||||
|
update_parent_list(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Entry
|
||||||
|
data_map=map_signal
|
||||||
|
schema=schema
|
||||||
|
id=unique_id
|
||||||
|
on_delete=on_delete
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ActionButton on:click = add_entry>"Add entry"</ActionButton>
|
<ActionButton on:click=add_entry>"Add entry"</ActionButton>
|
||||||
<ActionButton on:click=move |_| {
|
|
||||||
console_log(format!("Entry list: {:?}", data_reactive.get()).as_str());
|
|
||||||
}
|
|
||||||
>"Print Info"</ActionButton>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,20 +207,35 @@ fn Entry(
|
|||||||
/// The data map that contains all information. Data is found within this map using the `member`
|
/// The data map that contains all information. Data is found within this map using the `member`
|
||||||
/// props. This map should be unique to this entry; in other words, it should be contained and
|
/// props. This map should be unique to this entry; in other words, it should be contained and
|
||||||
/// passed down from within the entry list in the parent `MultiEntry` component.
|
/// passed down from within the entry list in the parent `MultiEntry` component.
|
||||||
#[prop()]
|
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
data_map: RwSignal<HashMap<String, ValueType>>,
|
|
||||||
/// The schema of this entry. Should contain all information about what inputs to use and what
|
/// The schema of this entry. Should contain all information about what inputs to use and what
|
||||||
/// data members those inputs modify.
|
/// data members those inputs modify.
|
||||||
#[prop()]
|
#[prop(into)] schema: Signal<Vec<InputType>>,
|
||||||
schema: Vec<InputType>,
|
/// The unique ID for this entry.
|
||||||
|
#[prop(into)] id: Signal<String>,
|
||||||
|
/// The function to run when the delete button is clicked.
|
||||||
|
#[prop(into)] on_delete: Callback<()>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<div class="flex flex-col flex-1 p-2 rounded-sm transition-shadow shadow-sm hover:shadow-lg/30">
|
<div class="relative rounded-sm transition-shadow shadow-sm hover:shadow-lg/30">
|
||||||
{schema.iter()
|
// Delete button
|
||||||
.map(|input_type| {
|
<div
|
||||||
input_type.clone().into_view(data_map.clone())
|
class="absolute rounded-full w-8 h-8 -top-4 -right-4 flex items-center justify-center bg-gray-100 text-gray-700 hover:text-red-600 transition-all cursor-pointer"
|
||||||
}).collect_view()
|
on:click=move |_| on_delete.run(())
|
||||||
}
|
>
|
||||||
|
<Icon icon=icondata::FaTrashCanRegular />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Input section
|
||||||
|
<div class="flex flex-col flex-1 p-2">
|
||||||
|
{move || {
|
||||||
|
schema
|
||||||
|
.get()
|
||||||
|
.iter()
|
||||||
|
.map(|input_type| input_type.clone().into_view(data_map.clone(), id.get()))
|
||||||
|
.collect_view()
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mod sidebar;
|
||||||
|
pub use sidebar::*;
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_router::components::{Outlet, A};
|
||||||
|
use leptos_router::hooks::use_location;
|
||||||
|
use crate::common::{SubTabInfo, TabInfo};
|
||||||
|
|
||||||
|
/// # Tab Sidebar Component
|
||||||
|
///
|
||||||
|
/// This component creates a list of tabs that correspond to the given `Vec<TabInfo>`.
|
||||||
|
/// There are a few things to note:
|
||||||
|
///
|
||||||
|
/// - This component defines an [`Outlet`], meaning that it is designed to display the route
|
||||||
|
/// information on its own. If you intend to use this on a page, the page should not also contain
|
||||||
|
/// an `Outlet` component unless it needs to render its own nested children.
|
||||||
|
/// - The "selected" appearance for the individual tabs work best when their paths are at the same
|
||||||
|
/// depth. For example:
|
||||||
|
/// ```
|
||||||
|
/// let good_list = vec! [
|
||||||
|
/// TabInfo {
|
||||||
|
/// ...,
|
||||||
|
/// path: "/default/path"
|
||||||
|
/// },
|
||||||
|
/// TabInfo {
|
||||||
|
/// ...,
|
||||||
|
/// path: "/good/path"
|
||||||
|
/// }
|
||||||
|
/// ];
|
||||||
|
///
|
||||||
|
/// let bad_list = vec! [
|
||||||
|
/// TabInfo {
|
||||||
|
/// ...,
|
||||||
|
/// path: "/default/path"
|
||||||
|
/// },
|
||||||
|
/// TabInfo {
|
||||||
|
/// ...,
|
||||||
|
/// path: "/bad/path/here"
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Example usage in a view (assuming the good list from above):
|
||||||
|
/// ```
|
||||||
|
/// view! {
|
||||||
|
/// <TabSidebarList
|
||||||
|
/// tabs=good_list
|
||||||
|
/// />
|
||||||
|
/// // Remember, no Outlets!
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[component]
|
||||||
|
pub fn TabSidebarList(
|
||||||
|
#[prop(into)] base_path: Signal<String>,
|
||||||
|
#[prop(into)] tabs: Signal<Vec<TabInfo>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
// Create a series of tabs based on a given list of names and paths.
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-row w-full">
|
||||||
|
<nav class="flex flex-col bg-red-900 w-64 overflow-scroll">
|
||||||
|
<For
|
||||||
|
each=move || tabs.get()
|
||||||
|
key=|info| info.text.clone()
|
||||||
|
let(TabInfo { text, path, sub_paths })
|
||||||
|
>
|
||||||
|
<SidebarTab base_path=base_path text=text path=path sub_paths=sub_paths />
|
||||||
|
</For>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex flex-1 overflow-scroll">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn SidebarTab(
|
||||||
|
#[prop(into)] base_path: Signal<String>,
|
||||||
|
#[prop(into)] text: Signal<String>,
|
||||||
|
#[prop(into)] path: Signal<String>,
|
||||||
|
#[prop(into)] sub_paths: Signal<Vec<SubTabInfo>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let location = use_location();
|
||||||
|
|
||||||
|
let full_path = Memo::new(move |_| {
|
||||||
|
format!("/{}/{}", base_path.get(), path.get())
|
||||||
|
});
|
||||||
|
|
||||||
|
let selected = Memo::new(move |_| {
|
||||||
|
let current_path = location.pathname.get();
|
||||||
|
|
||||||
|
// There's extra logic here as well, but for now this works.
|
||||||
|
current_path.starts_with(&full_path.get())
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="transition-all duration-300"
|
||||||
|
class=(["bg-transparent", "text-white"], move || !selected.get())
|
||||||
|
class=(["bg-white", "text-black"], move || selected.get())
|
||||||
|
>
|
||||||
|
<A href=move || path.get()>
|
||||||
|
<div class="p-3 transition-bg font-bold">{text}</div>
|
||||||
|
</A>
|
||||||
|
<div
|
||||||
|
class="grid transition-all ease-in-out"
|
||||||
|
style:grid-template-rows=move || if selected.get() { "1fr" } else { "0fr" }
|
||||||
|
>
|
||||||
|
<div class="overflow-hidden min-h-0">
|
||||||
|
<For
|
||||||
|
each=move || sub_paths.get()
|
||||||
|
key=|info| info.text.clone()
|
||||||
|
let(SubTabInfo { text, path })
|
||||||
|
>
|
||||||
|
<SidebarSubTab parent_path=full_path text=text path=path />
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn SidebarSubTab(
|
||||||
|
#[prop(into)] parent_path: Signal<String>,
|
||||||
|
#[prop(into)] text: Signal<String>,
|
||||||
|
#[prop(into)] path: Signal<String>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let full_path = Memo::new(move |_| {
|
||||||
|
format!("{}/{}", parent_path.get(), path.get())
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<A
|
||||||
|
href=move || full_path.get()
|
||||||
|
{..}
|
||||||
|
class="block p-3 pl-10 transition-all aria-[current]:font-bold"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</A>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use crate::components::{ActionButton, Header};
|
|||||||
|
|
||||||
/// Indicates the current state of an input.
|
/// Indicates the current state of an input.
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub enum ValidationState {
|
pub enum ValidationState {
|
||||||
/// A valid input.
|
/// A valid input.
|
||||||
#[default]
|
#[default]
|
||||||
@@ -149,15 +150,8 @@ pub fn ValidatedForm(
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<form
|
<form class="flex flex-col gap-2 py-7" novalidate on:submit=submit_success_event>
|
||||||
class="flex flex-col gap-2"
|
<Header title=title description=description />
|
||||||
novalidate
|
|
||||||
on:submit=submit_success_event
|
|
||||||
>
|
|
||||||
<Header
|
|
||||||
title=title
|
|
||||||
description=description
|
|
||||||
/>
|
|
||||||
{children()}
|
{children()}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdminHomePage() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-col gap-4 items-center m-4">
|
||||||
|
<div class="font-bold text-3xl">"Admin Home Page"</div>
|
||||||
|
<div class="text-lg">"This is just a placeholder for now."</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
mod shell;
|
||||||
|
mod home_page;
|
||||||
|
mod provider_page;
|
||||||
|
mod scholarship_page;
|
||||||
|
mod utils_page;
|
||||||
|
|
||||||
|
pub use shell::*;
|
||||||
|
pub use home_page::*;
|
||||||
|
pub use provider_page::*;
|
||||||
|
pub use scholarship_page::*;
|
||||||
|
pub use utils_page::*;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use leptos::logging::log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::common::ValueType;
|
||||||
|
use crate::components::{NavListInfo, SelectableNavList};
|
||||||
|
use crate::pages::api::get_all_providers;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdminProviderPage() -> impl IntoView {
|
||||||
|
let selected_items = RwSignal::new(Vec::new());
|
||||||
|
let provider_list = RwSignal::new(Vec::new());
|
||||||
|
|
||||||
|
let provider_trigger = Trigger::new();
|
||||||
|
let provider_resource = Resource::new(
|
||||||
|
move || provider_trigger.track(),
|
||||||
|
async move |_| {
|
||||||
|
get_all_providers().await
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
if let Some(Err(err)) = provider_resource.get() {
|
||||||
|
log!("Found error: {}", err.to_string());
|
||||||
|
}
|
||||||
|
let Some(Ok(list)) = provider_resource.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let members_list = vec![
|
||||||
|
"contact_email".to_string(),
|
||||||
|
"first_name".to_string(),
|
||||||
|
"last_name".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convert the list into a series of NavListInfo
|
||||||
|
let converted = list
|
||||||
|
.into_iter()
|
||||||
|
.map(|info| {
|
||||||
|
let mut text_list = vec![];
|
||||||
|
members_list.iter().for_each(|member| {
|
||||||
|
let val = info.get(member)
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.as_string()
|
||||||
|
.ok().flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
text_list.push(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
let subject = info.get("subject")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
NavListInfo {
|
||||||
|
text: text_list,
|
||||||
|
id: subject.clone(),
|
||||||
|
slug: subject,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
provider_list.set(converted);
|
||||||
|
});
|
||||||
|
|
||||||
|
let headers = vec![
|
||||||
|
"Email",
|
||||||
|
"First Name",
|
||||||
|
"Last Name"
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-col gap-4 items-stretch mt-4">
|
||||||
|
<div class="font-bold text-3xl self-center">"Provider View"</div>
|
||||||
|
<SelectableNavList
|
||||||
|
selected=selected_items
|
||||||
|
name="providers-nav-list"
|
||||||
|
items=provider_list
|
||||||
|
headers=headers
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdminScholarshipPage() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-col gap-4">
|
||||||
|
<div class="font-bold text-3xl">
|
||||||
|
"Scholarships View"
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
"This is a placeholder for now. It will show a list of all scholarships."
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::common::TabInfo;
|
||||||
|
use crate::components::{Banner, TabSidebarList};
|
||||||
|
use crate::components::login::AdminLoginContext;
|
||||||
|
|
||||||
|
/// # Admin Shell Component
|
||||||
|
///
|
||||||
|
/// This shell should keep track of pretty much everything that changes across the admin page. It
|
||||||
|
/// will provide the login context, the banner, and the tabs.
|
||||||
|
#[component]
|
||||||
|
pub fn AdminShell() -> impl IntoView {
|
||||||
|
// Create login context
|
||||||
|
view! {
|
||||||
|
<AdminLoginContext>
|
||||||
|
<Banner title="R15 Scholarships Admin" logo="/PHS_Stacked_Acronym.png" />
|
||||||
|
// We'll put the tabs here.
|
||||||
|
<TabSidebarList
|
||||||
|
base_path="admin"
|
||||||
|
tabs=vec![
|
||||||
|
TabInfo::new("Home", "home", None),
|
||||||
|
TabInfo::new("Providers", "providers", None),
|
||||||
|
TabInfo::new("Scholarships", "scholarships", None),
|
||||||
|
TabInfo::new("Utilities", "utilities", None),
|
||||||
|
]
|
||||||
|
/>
|
||||||
|
</AdminLoginContext>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
use chrono::DateTime;
|
||||||
|
use crate::components::ActionButton;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::common::{DateInfo, DateRange};
|
||||||
|
use crate::pages::api::{CreateDates, CreateTestComparisons};
|
||||||
|
|
||||||
|
fn get_important_dates() -> Vec<DateInfo> {
|
||||||
|
vec! [
|
||||||
|
DateInfo {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
title: "Provider Forms Open".to_string(),
|
||||||
|
date: DateRange::Range(
|
||||||
|
DateTime::parse_from_rfc3339("2026-01-20T07:15:00-05:00").unwrap(),
|
||||||
|
DateTime::parse_from_rfc3339("2026-03-03T23:59:00-05:00").unwrap()
|
||||||
|
),
|
||||||
|
description: "".to_string(),
|
||||||
|
},
|
||||||
|
DateInfo {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
title: "Student Forms Open".to_string(),
|
||||||
|
date: DateRange::Range(
|
||||||
|
DateTime::parse_from_rfc3339("2026-03-03T07:15:00-05:00").unwrap(),
|
||||||
|
DateTime::parse_from_rfc3339("2026-03-27T14:05:00-05:00").unwrap()
|
||||||
|
),
|
||||||
|
description: "".to_string(),
|
||||||
|
},
|
||||||
|
DateInfo {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
title: "Scholarship Decision Window".to_string(),
|
||||||
|
date: DateRange::Range(
|
||||||
|
DateTime::parse_from_rfc3339("2026-04-10T07:15:00-05:00").unwrap(),
|
||||||
|
DateTime::parse_from_rfc3339("2026-05-01T23:59:00-05:00").unwrap()
|
||||||
|
),
|
||||||
|
description: "Start date may change if available sooner.".to_string(),
|
||||||
|
},
|
||||||
|
DateInfo {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
title: "PHS Scholarship Committee".to_string(),
|
||||||
|
date: DateRange::Single(
|
||||||
|
DateTime::parse_from_rfc3339("2026-05-18T00:00:00-05:00").unwrap(),
|
||||||
|
),
|
||||||
|
description: "Time TBD".to_string(),
|
||||||
|
},
|
||||||
|
DateInfo {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
title: "Scholarship and Awards Night".to_string(),
|
||||||
|
date: DateRange::Single(
|
||||||
|
DateTime::parse_from_rfc3339("2026-06-09T00:00:00-05:00").unwrap(),
|
||||||
|
),
|
||||||
|
description: "Time TBD".to_string(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn create_scholarship_export() -> Result<Vec<u8>, ServerFnError> {
|
||||||
|
// Exports olarship information - specifically the name of the scholarship, the name of the
|
||||||
|
// provider, and the scholarship's requirements.
|
||||||
|
// This means we'll need to get all the relations, all the scholarships, and all the contact info.
|
||||||
|
use crate::utils::server::create_dynamo_client;
|
||||||
|
use aws_sdk_dynamodb::client::Client;
|
||||||
|
use crate::pages::api::{get_comparison_info, SCHOLARSHIPS_TABLE, PROVIDER_CONTACT_TABLE};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::common::ValueType;
|
||||||
|
|
||||||
|
let scan_table_items = async |client: &Client, table_name: &str| {
|
||||||
|
client.scan()
|
||||||
|
.table_name(table_name)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.ok().and_then(|res| res.items)
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||||
|
struct ScholarshipInfo {
|
||||||
|
name: String,
|
||||||
|
provider_name: String,
|
||||||
|
relations: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let scholarships = dbg!(scan_table_items(&client, SCHOLARSHIPS_TABLE)
|
||||||
|
.await
|
||||||
|
.iter().map(|item| {
|
||||||
|
item.into_iter()
|
||||||
|
.map(|(k, v)| (k.clone(), ValueType::from(v)))
|
||||||
|
.collect()
|
||||||
|
}).collect::<Vec<HashMap<String, ValueType>>>()
|
||||||
|
);
|
||||||
|
let contacts = dbg!(
|
||||||
|
scan_table_items(&client, PROVIDER_CONTACT_TABLE).await
|
||||||
|
.iter().map(|item| {
|
||||||
|
item.into_iter()
|
||||||
|
.map(|(k, v)| (k.clone(), ValueType::from(v)))
|
||||||
|
.collect()
|
||||||
|
}).collect::<Vec<HashMap<String, ValueType>>>()
|
||||||
|
);
|
||||||
|
let db_relations = dbg!(
|
||||||
|
get_comparison_info().await.unwrap_or_default()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build a new structure containing some of the scholarship's information.
|
||||||
|
let resolved = scholarships.into_iter()
|
||||||
|
.map(|scholarship| {
|
||||||
|
// We just want the scholarship's name, the relations, and the provider's name.
|
||||||
|
// In here, we're going to resolve all the scholarship's information to provide this in
|
||||||
|
// a readable format.
|
||||||
|
let name = scholarship.get("name")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// The list of relations - we're going to search the master list for each comparison's
|
||||||
|
// name. It's structured as a map of lists, or a ValueType::Map(Option<HashMap<String, ValueType::List(Option<Vec<ValueType::String()>>)>>)
|
||||||
|
let relations = scholarship.get("requirements")
|
||||||
|
.unwrap_or(&ValueType::Map(None))
|
||||||
|
.as_map().ok().flatten()
|
||||||
|
.unwrap_or_default() // We have our map of categories, now we need all the values inside each category.
|
||||||
|
.iter()
|
||||||
|
.map(|(_, list)| {
|
||||||
|
list.as_list().ok().flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<ValueType>>() // This is a Vec<ValueType::String>
|
||||||
|
.iter().map(|val| val.clone().as_string().ok().flatten().unwrap_or_default())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
// The provider ID, which we'll use to search for the provider's name.
|
||||||
|
let provider_id = scholarship.get("provider_id")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// This is a mess, we'll clean it up later.
|
||||||
|
let parsed_relations = relations
|
||||||
|
.iter()
|
||||||
|
.filter_map(|relation| {
|
||||||
|
db_relations.iter().find(|search| {
|
||||||
|
search.id == *relation
|
||||||
|
})
|
||||||
|
.and_then(|search| Some(search.display_text.clone()))
|
||||||
|
})
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
ScholarshipInfo {
|
||||||
|
name,
|
||||||
|
provider_name: provider_id,
|
||||||
|
relations: parsed_relations,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<ScholarshipInfo>>();
|
||||||
|
|
||||||
|
// TODO Convert into CSV and send to client. For now I think I'm just going to serialize to json and print it.
|
||||||
|
leptos::logging::log!("Scholarships list: {:?}", resolved);
|
||||||
|
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdminUtilsPage() -> impl IntoView {
|
||||||
|
let create_comparisons = ServerAction::<CreateTestComparisons>::new();
|
||||||
|
let create_dates = ServerAction::<CreateDates>::new();
|
||||||
|
let create_export = ServerAction::<CreateScholarshipExport>::new();
|
||||||
|
|
||||||
|
let on_click_comparisons = move |_| {
|
||||||
|
create_comparisons.dispatch(CreateTestComparisons{});
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_click_dates = move |_| {
|
||||||
|
create_dates.dispatch(CreateDates{dates: get_important_dates()});
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_click_export = move |_| {
|
||||||
|
create_export.dispatch(CreateScholarshipExport{});
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="mx-auto px-6">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="self-centered text-lg mt-6">
|
||||||
|
"This utility page provides buttons to initialize the corresponding lists of information."
|
||||||
|
</div>
|
||||||
|
<ActionButton on:click=on_click_comparisons>"Create Comparisons"</ActionButton>
|
||||||
|
<ActionButton on:click=on_click_dates>"Create Dates"</ActionButton>
|
||||||
|
<ActionButton on:click=on_click_export>"Scholarship Export"</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}.into_any()
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::common::ValueType;
|
||||||
|
use leptos::logging::log;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
mod server_logic {
|
||||||
|
pub use crate::utils::server::create_dynamo_client;
|
||||||
|
pub use super::super::PROVIDER_CONTACT_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_all_providers() -> Result<Vec<HashMap<String, ValueType>>, ServerFnError> {
|
||||||
|
use server_logic::*;
|
||||||
|
|
||||||
|
// We want to get all provider information from the contacts table.
|
||||||
|
// This will be migrated to the new table structure when it happens.
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
log!("Scanning all provider information from {}", PROVIDER_CONTACT_TABLE);
|
||||||
|
|
||||||
|
client
|
||||||
|
.scan()
|
||||||
|
.table_name(PROVIDER_CONTACT_TABLE)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map(|output| {
|
||||||
|
if let Some(items) = output.items {
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| {
|
||||||
|
item.into_iter().map(|(key, attr)| {
|
||||||
|
let val = ValueType::from(&attr);
|
||||||
|
(key, val)
|
||||||
|
}).collect()
|
||||||
|
})
|
||||||
|
.collect::<Vec<HashMap<String, ValueType>>>()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(ServerFnError::from)
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@ use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use leptos::logging::log;
|
use leptos::logging::log;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use super::COMPARISONS_TABLE;
|
||||||
|
|
||||||
use crate::common::{
|
use crate::common::{
|
||||||
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
|
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
|
||||||
TextComparison, TextListComparison, ValueType,
|
TextComparison, TextListComparison, ValueType,
|
||||||
@@ -15,8 +18,6 @@ use std::collections::HashMap;
|
|||||||
use leptos::prelude::ServerFnError;
|
use leptos::prelude::ServerFnError;
|
||||||
use leptos::server;
|
use leptos::server;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
static COMPARISONS_TABLE: &str = "leptos-comparisons";
|
|
||||||
|
|
||||||
/// Helper function to create all sports comparisons. Only used to create initial
|
/// Helper function to create all sports comparisons. Only used to create initial
|
||||||
/// comparison data, and should be removed upon completion of a comparison editor page.
|
/// comparison data, and should be removed upon completion of a comparison editor page.
|
||||||
@@ -188,6 +189,17 @@ fn create_sports_comparisons() -> Vec<ComparisonData> {
|
|||||||
"Sports Participation",
|
"Sports Participation",
|
||||||
"Tennis",
|
"Tennis",
|
||||||
),
|
),
|
||||||
|
ComparisonData::new(
|
||||||
|
"sports_baseball",
|
||||||
|
"sports_participation",
|
||||||
|
ComparisonType::MapList(MapListComparison::FlattenToTextList(
|
||||||
|
"sport_name".to_string(),
|
||||||
|
Box::new(TextListComparison::Contains),
|
||||||
|
)),
|
||||||
|
ValueType::String(Some("Baseball".to_string())),
|
||||||
|
"Sports Participation",
|
||||||
|
"Baseball",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,12 +264,12 @@ fn create_major_comparisons() -> Vec<ComparisonData> {
|
|||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
fn make_comp_list() -> Vec<ComparisonData> {
|
fn make_comp_list() -> Vec<ComparisonData> {
|
||||||
let gpa_3 = ComparisonData::new(
|
let gpa_3 = ComparisonData::new(
|
||||||
"gpa_3",
|
"gpa_top_third",
|
||||||
"weighted_gpa",
|
"weighted_gpa",
|
||||||
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
|
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
|
||||||
ValueType::Number(Some(3.0.to_string())),
|
ValueType::Number(Some(6.264.to_string())),
|
||||||
"GPA Limits",
|
"GPA Limits",
|
||||||
"GPA >= 3.0",
|
"Top Third GPA",
|
||||||
);
|
);
|
||||||
|
|
||||||
let test_comp = ComparisonData::new(
|
let test_comp = ComparisonData::new(
|
||||||
@@ -271,13 +283,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
|||||||
|
|
||||||
let service_hours_20 = ComparisonData::new(
|
let service_hours_20 = ComparisonData::new(
|
||||||
"service_hours_20",
|
"service_hours_20",
|
||||||
"community_involvement",
|
"service_hours",
|
||||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
ComparisonType::Number(
|
||||||
"service_hours".to_string(),
|
NumberComparison::GreaterThanOrEqual
|
||||||
Box::new(NumberListComparison::Sum(Box::new(
|
),
|
||||||
NumberComparison::GreaterThanOrEqual,
|
|
||||||
))),
|
|
||||||
)),
|
|
||||||
ValueType::Number(Some(20.to_string())),
|
ValueType::Number(Some(20.to_string())),
|
||||||
"Community Service",
|
"Community Service",
|
||||||
"20+ service hours",
|
"20+ service hours",
|
||||||
@@ -285,13 +294,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
|||||||
|
|
||||||
let service_hours_25 = ComparisonData::new(
|
let service_hours_25 = ComparisonData::new(
|
||||||
"service_hours_25",
|
"service_hours_25",
|
||||||
"community_involvement",
|
"service_hours",
|
||||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
ComparisonType::Number(
|
||||||
"service_hours".to_string(),
|
NumberComparison::GreaterThanOrEqual
|
||||||
Box::new(NumberListComparison::Sum(Box::new(
|
),
|
||||||
NumberComparison::GreaterThanOrEqual,
|
|
||||||
))),
|
|
||||||
)),
|
|
||||||
ValueType::Number(Some(25.to_string())),
|
ValueType::Number(Some(25.to_string())),
|
||||||
"Community Service",
|
"Community Service",
|
||||||
"25+ service hours",
|
"25+ service hours",
|
||||||
@@ -299,13 +305,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
|||||||
|
|
||||||
let service_hours_30 = ComparisonData::new(
|
let service_hours_30 = ComparisonData::new(
|
||||||
"service_hours_30",
|
"service_hours_30",
|
||||||
"community_involvement",
|
"service_hours",
|
||||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
ComparisonType::Number(
|
||||||
"service_hours".to_string(),
|
NumberComparison::GreaterThanOrEqual
|
||||||
Box::new(NumberListComparison::Sum(Box::new(
|
),
|
||||||
NumberComparison::GreaterThanOrEqual,
|
|
||||||
))),
|
|
||||||
)),
|
|
||||||
ValueType::Number(Some(30.to_string())),
|
ValueType::Number(Some(30.to_string())),
|
||||||
"Community Service",
|
"Community Service",
|
||||||
"30+ service hours",
|
"30+ service hours",
|
||||||
@@ -356,6 +359,42 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
|||||||
"Family with Military Service",
|
"Family with Military Service",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let youth_baseball = ComparisonData::new(
|
||||||
|
"youth_baseball",
|
||||||
|
"youth_baseball",
|
||||||
|
ComparisonType::Text(TextComparison::Matches),
|
||||||
|
ValueType::String(Some("Yes".to_string())),
|
||||||
|
"Additional Eligibility Factors",
|
||||||
|
"Participated in Pomperaug Youth Baseball"
|
||||||
|
);
|
||||||
|
|
||||||
|
let aquatic_club = ComparisonData::new(
|
||||||
|
"aquatic_club",
|
||||||
|
"aquatic_club",
|
||||||
|
ComparisonType::Text(TextComparison::Matches),
|
||||||
|
ValueType::String(Some("Yes".to_string())),
|
||||||
|
"Additional Eligibility Factors",
|
||||||
|
"Participated in Panthers Aquatic Club"
|
||||||
|
);
|
||||||
|
|
||||||
|
let pea_member = ComparisonData::new(
|
||||||
|
"pea_member",
|
||||||
|
"pea_member",
|
||||||
|
ComparisonType::Text(TextComparison::Matches),
|
||||||
|
ValueType::String(Some("Yes".to_string())),
|
||||||
|
"Additional Eligibility Factors",
|
||||||
|
"Has a Parent in the PEA"
|
||||||
|
);
|
||||||
|
|
||||||
|
let music_program = ComparisonData::new(
|
||||||
|
"music_program",
|
||||||
|
"music_program",
|
||||||
|
ComparisonType::Text(TextComparison::Matches),
|
||||||
|
ValueType::String(Some("Yes".to_string())),
|
||||||
|
"Sports Participation",
|
||||||
|
"PHS Music Program"
|
||||||
|
);
|
||||||
|
|
||||||
let mut sports_comps = create_sports_comparisons();
|
let mut sports_comps = create_sports_comparisons();
|
||||||
let mut major_comps = create_major_comparisons();
|
let mut major_comps = create_major_comparisons();
|
||||||
|
|
||||||
@@ -370,6 +409,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
|||||||
attended_bas,
|
attended_bas,
|
||||||
midd_south,
|
midd_south,
|
||||||
family_military,
|
family_military,
|
||||||
|
youth_baseball,
|
||||||
|
aquatic_club,
|
||||||
|
pea_member,
|
||||||
|
music_program
|
||||||
];
|
];
|
||||||
|
|
||||||
comp_list.append(&mut sports_comps);
|
comp_list.append(&mut sports_comps);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use leptos::prelude::*;
|
|||||||
use crate::common::DateInfo;
|
use crate::common::DateInfo;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
static DATES_TABLE: &str = "leptos-dates";
|
use super::DATES_TABLE;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use aws_sdk_dynamodb::{
|
use aws_sdk_dynamodb::{
|
||||||
|
|||||||
+23
-3
@@ -1,7 +1,27 @@
|
|||||||
mod comparisons;
|
mod comparisons;
|
||||||
pub use comparisons::*;
|
|
||||||
mod scholarships;
|
mod scholarships;
|
||||||
pub use scholarships::*;
|
|
||||||
mod dates;
|
mod dates;
|
||||||
pub use dates::*;
|
mod admin;
|
||||||
|
mod providers;
|
||||||
|
pub mod students;
|
||||||
|
|
||||||
|
pub use comparisons::*;
|
||||||
|
pub use scholarships::*;
|
||||||
|
pub use dates::*;
|
||||||
|
pub use admin::*;
|
||||||
|
pub use providers::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub static COMPARISONS_TABLE: &str = "leptos-comparisons";
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub static DATES_TABLE: &str = "leptos-dates";
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub static SCHOLARSHIPS_TABLE: &str = "leptos-scholarships";
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub static MAIN_TABLE_NAME: &str = "scholarships-main";
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos::logging::debug_log;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use leptos::server_fn::codec::Json;
|
||||||
|
use crate::common::ValueType;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use crate::utils::server::{create_dynamo_client, into_attr_map};
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
|
||||||
|
use aws_sdk_dynamodb::types::AttributeValue;
|
||||||
|
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
debug_log!("Getting contact info for provider {}", id);
|
||||||
|
|
||||||
|
match client
|
||||||
|
.get_item()
|
||||||
|
.table_name(PROVIDER_CONTACT_TABLE)
|
||||||
|
.key("subject", AttributeValue::S(id))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(output) => {
|
||||||
|
let Some(item) = output.item else {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
};
|
||||||
|
|
||||||
|
let map = item.iter().map(|(key, val)| {
|
||||||
|
let val_type = ValueType::from(val);
|
||||||
|
|
||||||
|
(key.clone(), val_type)
|
||||||
|
}).collect::<HashMap<String, ValueType>>();
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||||
|
Err(ServerFnError::new(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(input = Json)]
|
||||||
|
pub async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> {
|
||||||
|
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let mut item = into_attr_map(contact_info);
|
||||||
|
item.insert("subject".to_owned(), aws_sdk_dynamodb::types::AttributeValue::S(id));
|
||||||
|
|
||||||
|
match client
|
||||||
|
.put_item()
|
||||||
|
.table_name(PROVIDER_CONTACT_TABLE)
|
||||||
|
.set_item(Some(item))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||||
|
Err(ServerFnError::new(msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,26 @@
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use aws_sdk_dynamodb::{
|
mod imports {
|
||||||
types::AttributeValue,
|
pub use leptos::logging::debug_log;
|
||||||
error::ProvideErrorMetadata,
|
pub use aws_sdk_dynamodb::{
|
||||||
};
|
types::AttributeValue,
|
||||||
#[cfg(feature = "ssr")]
|
error::ProvideErrorMetadata,
|
||||||
use leptos::leptos_dom::log;
|
};
|
||||||
#[cfg(feature = "ssr")]
|
pub use leptos::leptos_dom::log;
|
||||||
use uuid::Uuid;
|
pub use uuid::Uuid;
|
||||||
#[cfg(feature = "ssr")]
|
pub use crate::utils::server::{create_dynamo_client};
|
||||||
use crate::utils::server::{create_dynamo_client, into_attr_map};
|
pub use crate::common::ValueType;
|
||||||
#[cfg(feature = "ssr")]
|
pub use super::super::SCHOLARSHIPS_TABLE;
|
||||||
use crate::common::ValueType;
|
}
|
||||||
|
|
||||||
use crate::common::ExpandableInfo;
|
use crate::common::ExpandableInfo;
|
||||||
|
|
||||||
use leptos::prelude::ServerFnError;
|
use leptos::prelude::ServerFnError;
|
||||||
use leptos::server;
|
use leptos::server;
|
||||||
|
use leptos::server_fn::codec::Json;
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
static SCHOLARSHIPS_TABLE: &str = "leptos-scholarships";
|
|
||||||
|
|
||||||
#[server(GetScholarshipInfo)]
|
#[server(GetScholarshipInfo)]
|
||||||
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
|
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
// Perform the operation - we just want to return all data that's contained in this entry,
|
// Perform the operation - we just want to return all data that's contained in this entry,
|
||||||
@@ -50,8 +49,9 @@ pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(CreateScholarshipInfo)]
|
#[server(CreateScholarshipInfo, input = Json)]
|
||||||
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> {
|
pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
@@ -75,8 +75,10 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
|
|||||||
|
|
||||||
#[server(GetAllScholarshipInfo)]
|
#[server(GetAllScholarshipInfo)]
|
||||||
pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
log!("Getting all scholarship info");
|
debug_log!("Getting all scholarship info");
|
||||||
match client
|
match client
|
||||||
.scan()
|
.scan()
|
||||||
.table_name(SCHOLARSHIPS_TABLE)
|
.table_name(SCHOLARSHIPS_TABLE)
|
||||||
@@ -100,9 +102,11 @@ pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnE
|
|||||||
/// Gets all scholarships that are associated with the given scholarship provider's ID.
|
/// Gets all scholarships that are associated with the given scholarship provider's ID.
|
||||||
#[server(GetProviderScholarships)]
|
#[server(GetProviderScholarships)]
|
||||||
pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
log!("Getting provider scholarships for provider with ID {:?}", provider_id);
|
debug_log!("Getting provider scholarships for provider with ID {:?}", provider_id);
|
||||||
|
|
||||||
match client
|
match client
|
||||||
.scan()
|
.scan()
|
||||||
@@ -129,9 +133,11 @@ pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<Expand
|
|||||||
/// Creates a new scholarship with a unique ID, and then returns that ID.
|
/// Creates a new scholarship with a unique ID, and then returns that ID.
|
||||||
#[server(RegisterScholarship)]
|
#[server(RegisterScholarship)]
|
||||||
pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> {
|
pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
log!("Creating scholarship for provider with ID {:?}", provider_id);
|
debug_log!("Creating scholarship for provider with ID {:?}", provider_id);
|
||||||
|
|
||||||
let mut current_uuid = Uuid::new_v4().to_string();
|
let mut current_uuid = Uuid::new_v4().to_string();
|
||||||
let mut item = ExpandableInfo::new(current_uuid.clone());
|
let mut item = ExpandableInfo::new(current_uuid.clone());
|
||||||
@@ -173,9 +179,11 @@ pub async fn register_scholarship(provider_id: String) -> Result<String, ServerF
|
|||||||
/// Deletes a provider's scholarship given their provider ID and scholarship ID.
|
/// Deletes a provider's scholarship given their provider ID and scholarship ID.
|
||||||
#[server(DeleteProviderScholarship)]
|
#[server(DeleteProviderScholarship)]
|
||||||
pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: String) -> Result<(), ServerFnError> {
|
pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: String) -> Result<(), ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
log!("Deleting scholarship with ID {:?} for provider with ID {:?}", scholarship_id, provider_id);
|
debug_log!("Deleting scholarship with ID {:?} for provider with ID {:?}", scholarship_id, provider_id);
|
||||||
|
|
||||||
// When we delete a scholarship, we need to ensure that the provider's ID matches the scholarship,
|
// When we delete a scholarship, we need to ensure that the provider's ID matches the scholarship,
|
||||||
// otherwise everyone can delete anyone else's scholarships.
|
// otherwise everyone can delete anyone else's scholarships.
|
||||||
@@ -197,3 +205,29 @@ pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_all_scholarships() -> Result<Vec<std::collections::HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
client
|
||||||
|
.scan()
|
||||||
|
.table_name(SCHOLARSHIPS_TABLE)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map(|output| {
|
||||||
|
let Some(items) = output.items else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
items.iter().map(|item| {
|
||||||
|
// Each item should be simply converted from an AttributeValue to a ValueType.
|
||||||
|
item.iter().map(|(k, v)| (k.clone(), ValueType::from(v)))
|
||||||
|
.collect::<HashMap<String, ValueType>>()
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.map_err(ServerFnError::from)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos::server_fn::codec::Json;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
mod imports {
|
||||||
|
pub use crate::utils::server::*;
|
||||||
|
pub use aws_sdk_dynamodb::types::AttributeValue;
|
||||||
|
pub use super::super::MAIN_TABLE_NAME;
|
||||||
|
pub use crate::common::ValueType;
|
||||||
|
pub use std::collections::HashMap;
|
||||||
|
pub use leptos::logging::debug_log;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(input = Json)]
|
||||||
|
pub async fn put_student_data(
|
||||||
|
subject: String,
|
||||||
|
data_type: String,
|
||||||
|
data_map: std::collections::HashMap<String, crate::common::ValueType>
|
||||||
|
) -> Result<(), ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let mut data_map_attr = into_attr_map(data_map);
|
||||||
|
data_map_attr.insert(
|
||||||
|
"HK".into(),
|
||||||
|
AttributeValue::S(format!("STUDENT#{}", subject))
|
||||||
|
);
|
||||||
|
data_map_attr.insert(
|
||||||
|
"SK".into(),
|
||||||
|
AttributeValue::S(data_type.to_uppercase())
|
||||||
|
);
|
||||||
|
|
||||||
|
debug_log!("Inserting this item: {:?}", data_map_attr);
|
||||||
|
|
||||||
|
client
|
||||||
|
.put_item()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.set_item(Some(data_map_attr))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(ServerFnError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_student_data(
|
||||||
|
subject: String,
|
||||||
|
data_type: String,
|
||||||
|
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
client
|
||||||
|
.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.key_condition_expression("HK = :hk AND SK = :sk")
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject)))
|
||||||
|
.expression_attribute_values(":sk", AttributeValue::S(data_type.to_uppercase()))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map(|output| {
|
||||||
|
// Map the output to a HashMap<String, ValueType>
|
||||||
|
let Some(list) = output.items else {
|
||||||
|
return HashMap::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
let _ = list
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.for_each(|(k, v)| {
|
||||||
|
map.insert(k, ValueType::from(&v));
|
||||||
|
});
|
||||||
|
|
||||||
|
map
|
||||||
|
})
|
||||||
|
.map_err(ServerFnError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets and flattens all of a student's information from the database, regardless of their defined
|
||||||
|
/// sort key.
|
||||||
|
///
|
||||||
|
/// Note: if sorted data from the `put_student_data` function contains the same fields, this
|
||||||
|
/// function will have unexpected behavior. For example, if a `first_name` field appears in multiple
|
||||||
|
/// locations in the database, all for this single student, they will overwrite each other in no
|
||||||
|
/// specific order.
|
||||||
|
#[server]
|
||||||
|
pub async fn get_all_student_data(
|
||||||
|
subject: String
|
||||||
|
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
client.query()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.key_condition_expression("HK = :hk")
|
||||||
|
.expression_attribute_values(":hk", AttributeValue::S(format!("STUDENT#{}", subject)))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map(|output| {
|
||||||
|
let Some(items) = output.items else {
|
||||||
|
return HashMap::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
items.iter().flatten()
|
||||||
|
.map(|(|k, v)| (k.clone(), ValueType::from(v)))
|
||||||
|
.collect::<HashMap<String, ValueType>>()
|
||||||
|
})
|
||||||
|
.map_err(ServerFnError::from)
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ pub fn ComparisonTestPage() -> impl IntoView {
|
|||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let comparison_outcome = match comp_opt {
|
let comparison_outcome = match comp_opt {
|
||||||
Some(comp) => comp.compare(&student),
|
Some(comp) => comp.compare(&student.data),
|
||||||
_ => Err("".to_string()),
|
_ => Err("".to_string()),
|
||||||
};
|
};
|
||||||
match comparison_outcome {
|
match comparison_outcome {
|
||||||
|
|||||||
+2
-14
@@ -12,7 +12,7 @@ use crate::common::ExpandableInfo;
|
|||||||
use crate::pages::UnauthenticatedPage;
|
use crate::pages::UnauthenticatedPage;
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
|
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
|
||||||
Select, TextFieldType, ValidatedForm
|
TextFieldType, ValidatedForm
|
||||||
};
|
};
|
||||||
use crate::input;
|
use crate::input;
|
||||||
use leptos::leptos_dom::logging::console_log;
|
use leptos::leptos_dom::logging::console_log;
|
||||||
@@ -329,21 +329,9 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
required=true
|
required=true
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
|
||||||
<Select
|
|
||||||
label="Gender:"
|
|
||||||
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| s.to_owned())
|
|
||||||
.collect()
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="gender"
|
|
||||||
data_map=form_data
|
|
||||||
required=true
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
<Row>
|
||||||
<MultiEntry
|
<MultiEntry
|
||||||
|
label="Testing community involvement:"
|
||||||
data_map=form_data
|
data_map=form_data
|
||||||
data_member="community_involvement"
|
data_member="community_involvement"
|
||||||
schema=vec![
|
schema=vec![
|
||||||
|
|||||||
+4
-3
@@ -5,14 +5,15 @@ mod home_page;
|
|||||||
mod loaner_page;
|
mod loaner_page;
|
||||||
mod provider_portal;
|
mod provider_portal;
|
||||||
mod scholarship_info;
|
mod scholarship_info;
|
||||||
mod test_page;
|
|
||||||
mod unauthenticated_page;
|
mod unauthenticated_page;
|
||||||
mod provider_contact;
|
mod provider_contact;
|
||||||
mod auth_callback;
|
mod auth_callback;
|
||||||
mod provider_applicants;
|
mod provider_applicants;
|
||||||
|
mod admin;
|
||||||
|
pub(crate) mod student;
|
||||||
|
|
||||||
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::*,
|
unauthenticated_page::*, scholarship_info::*, provider_contact::*,
|
||||||
auth_callback::*, provider_applicants::*
|
auth_callback::*, provider_applicants::*, admin::*, student::*,
|
||||||
};
|
};
|
||||||
@@ -8,7 +8,9 @@ pub fn ApplicantsPageFallback() -> impl IntoView {
|
|||||||
<div class="flex flex-col gap-4 mt-3 items-center justify-center">
|
<div class="flex flex-col gap-4 mt-3 items-center justify-center">
|
||||||
<h1 class="text-3xl font-bold">"Applicants Page"</h1>
|
<h1 class="text-3xl font-bold">"Applicants Page"</h1>
|
||||||
|
|
||||||
<p>"This page is under construction! It will show all the students that are eligible for your scholarship(s)."</p>
|
<p>
|
||||||
|
"This page is under construction! It will show all the students that are eligible for your scholarship(s)."
|
||||||
|
</p>
|
||||||
<p>"Come back soon!"</p>
|
<p>"Come back soon!"</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,11 @@
|
|||||||
#[cfg(feature = "ssr")]
|
use std::collections::HashMap;
|
||||||
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::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ValidatedForm};
|
||||||
use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
|
||||||
use crate::pages::UnauthenticatedPage;
|
use crate::pages::UnauthenticatedPage;
|
||||||
use crate::utils::get_user_claims;
|
use crate::utils::get_user_claims;
|
||||||
|
use super::api::{get_provider_contact, PutProviderContact};
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
|
|
||||||
|
|
||||||
#[server]
|
|
||||||
async fn get_provider_contact(id: String) -> Result<HashMap<String, ValueType>, ServerFnError> {
|
|
||||||
use aws_sdk_dynamodb::types::AttributeValue;
|
|
||||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
|
||||||
|
|
||||||
log!("Getting contact info for provider {}", id);
|
|
||||||
|
|
||||||
match client
|
|
||||||
.get_item()
|
|
||||||
.table_name(PROVIDER_CONTACT_TABLE)
|
|
||||||
.key("subject", AttributeValue::S(id))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(output) => {
|
|
||||||
let Some(item) = output.item else {
|
|
||||||
return Ok(HashMap::new());
|
|
||||||
};
|
|
||||||
|
|
||||||
let map = item.iter().map(|(key, val)| {
|
|
||||||
let val_type = ValueType::from(val);
|
|
||||||
|
|
||||||
(key.clone(), val_type)
|
|
||||||
}).collect::<HashMap<String, ValueType>>();
|
|
||||||
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
|
||||||
Err(ServerFnError::new(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[server]
|
|
||||||
async fn put_provider_contact(id: String, contact_info: HashMap<String, ValueType>) -> Result<(), ServerFnError> {
|
|
||||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
|
||||||
|
|
||||||
let mut item = into_attr_map(contact_info);
|
|
||||||
item.insert("subject".to_owned(), aws_sdk_dynamodb::types::AttributeValue::S(id));
|
|
||||||
|
|
||||||
match client
|
|
||||||
.put_item()
|
|
||||||
.table_name(PROVIDER_CONTACT_TABLE)
|
|
||||||
.set_item(Some(item))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(err) => {
|
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
|
||||||
Err(ServerFnError::new(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ProviderContactPage() -> impl IntoView {
|
pub fn ProviderContactPage() -> impl IntoView {
|
||||||
@@ -186,7 +120,7 @@ pub fn ProviderContactPage() -> impl IntoView {
|
|||||||
<Row>
|
<Row>
|
||||||
<OutlinedTextField
|
<OutlinedTextField
|
||||||
label="Contact Email:"
|
label="Contact Email:"
|
||||||
placeholder="student@region15.org"
|
placeholder="example@mymailservice.com"
|
||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
data_member="contact_email"
|
data_member="contact_email"
|
||||||
data_map=contact_info
|
data_map=contact_info
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use leptos::either::Either;
|
use leptos::either::Either;
|
||||||
use leptos::html::Dialog;
|
use leptos::html::Dialog;
|
||||||
use leptos::logging::log;
|
use leptos::logging::debug_log;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||||
use leptos_router::hooks::{use_navigate, use_params};
|
use leptos_router::hooks::{use_navigate, use_params};
|
||||||
@@ -111,7 +111,7 @@ fn ScholarshipList(
|
|||||||
(refresh_token.get(), provider_id.get())
|
(refresh_token.get(), provider_id.get())
|
||||||
},
|
},
|
||||||
|(_, provider_id)| async move {
|
|(_, provider_id)| async move {
|
||||||
log!("Fetching from scholarship API.");
|
debug_log!("Fetching from scholarship API.");
|
||||||
let Some(provider_id) = provider_id else {
|
let Some(provider_id) = provider_id else {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
};
|
};
|
||||||
@@ -179,7 +179,7 @@ fn ScholarshipList(
|
|||||||
.get()
|
.get()
|
||||||
.map(|s| s.subject.clone())
|
.map(|s| s.subject.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
log!("Deleting scholarship with subject {}", subject);
|
debug_log!("Deleting scholarship with subject {}", subject);
|
||||||
delete_action
|
delete_action
|
||||||
.dispatch(DeleteProviderScholarship {
|
.dispatch(DeleteProviderScholarship {
|
||||||
scholarship_id: subject,
|
scholarship_id: subject,
|
||||||
@@ -385,10 +385,10 @@ fn ScholarshipForm(
|
|||||||
if let Some(Ok(scholarship)) = scholarship_info.get() {
|
if let Some(Ok(scholarship)) = scholarship_info.get() {
|
||||||
// Get the default chips data.
|
// Get the default chips data.
|
||||||
let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") {
|
let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") {
|
||||||
log!("Found requirements map: {:?}", map);
|
debug_log!("Found requirements map: {:?}", map);
|
||||||
map.clone()
|
map.clone()
|
||||||
} else {
|
} else {
|
||||||
log!("Couldn't find requirements map in scholarship info. Using empty map.");
|
debug_log!("Couldn't find requirements map in scholarship info. Using empty map.");
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -496,7 +496,7 @@ fn ScholarshipForm(
|
|||||||
info.data = form_data.get();
|
info.data = form_data.get();
|
||||||
info.data.insert("requirements".to_string(), ValueType::Map(Some(chips_data.get())));
|
info.data.insert("requirements".to_string(), ValueType::Map(Some(chips_data.get())));
|
||||||
|
|
||||||
log!("Map values: {:?}", info.data);
|
debug_log!("Map values: {:?}", info.data);
|
||||||
submit_action.dispatch(CreateScholarshipInfo {
|
submit_action.dispatch(CreateScholarshipInfo {
|
||||||
info
|
info
|
||||||
});
|
});
|
||||||
@@ -505,7 +505,7 @@ fn ScholarshipForm(
|
|||||||
Effect::watch(
|
Effect::watch(
|
||||||
move || submit_action.value().get(),
|
move || submit_action.value().get(),
|
||||||
move |value, _, _| {
|
move |value, _, _| {
|
||||||
log!("Running submit_action effect");
|
debug_log!("Running submit_action effect");
|
||||||
let Some(result) = value else {
|
let Some(result) = value else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -639,7 +639,7 @@ fn ScholarshipForm(
|
|||||||
<Header
|
<Header
|
||||||
title="Eligibility Requirements"
|
title="Eligibility Requirements"
|
||||||
description="Choose any requirements that this scholarship may have. If there are no extra requirements,
|
description="Choose any requirements that this scholarship may have. If there are no extra requirements,
|
||||||
leave all options blank, however please note that this will apply all students to your scholarship."
|
leave all options blank, however please note that this will apply all students to your scholarship."
|
||||||
/>
|
/>
|
||||||
<Row>
|
<Row>
|
||||||
<ChipsList
|
<ChipsList
|
||||||
@@ -653,7 +653,7 @@ fn ScholarshipForm(
|
|||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<ChipsList
|
<ChipsList
|
||||||
label="Sports Participation"
|
label="Sports and Activities"
|
||||||
data_member="sports_participation"
|
data_member="sports_participation"
|
||||||
data_map=chips_data
|
data_map=chips_data
|
||||||
values=sports_ids
|
values=sports_ids
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::{Loading, OutlinedTextField, TextFieldType, ValidatedForm};
|
||||||
|
use crate::pages::student::form_setup::use_student_form;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentAcademicsPage() -> impl IntoView {
|
||||||
|
let controller = use_student_form("academics");
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show
|
||||||
|
when=move || controller.data_resource.get().is_some()
|
||||||
|
fallback=Loading
|
||||||
|
>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
<div class="flex flex-col flex-2 mt-6">
|
||||||
|
<ValidatedForm title="Academic Information" on_submit=controller.submit_action>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Unweighted GPA:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="unweighted_gpa"
|
||||||
|
placeholder="4.0"
|
||||||
|
required=true
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Weighted GPA:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="weighted_gpa"
|
||||||
|
placeholder="4.5"
|
||||||
|
required=true
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Highest SAT Score:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="sat_score"
|
||||||
|
placeholder="1600"
|
||||||
|
required=false
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Highest ACT Score:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="act_score"
|
||||||
|
placeholder="36"
|
||||||
|
required=false
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Academic Honors:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="academic_honors"
|
||||||
|
placeholder="TBD"
|
||||||
|
required=false
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
/>
|
||||||
|
</ValidatedForm>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn AdditionalPage() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-col gap-4 items-center m-4">
|
||||||
|
<div class="font-bold text-3xl">"Student Additional Forms"</div>
|
||||||
|
<div class="text-lg">
|
||||||
|
"This is just a placeholder for now. We'll show redirect buttons to each form."
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::{Loading, MultiEntry, ValidatedForm};
|
||||||
|
use crate::input;
|
||||||
|
use crate::pages::student::form_setup::use_student_form;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentAthleticsPage() -> impl IntoView {
|
||||||
|
let controller = use_student_form("athletics");
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show
|
||||||
|
when=move || controller.data_resource.get().is_some()
|
||||||
|
fallback=Loading
|
||||||
|
>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
<div class="flex flex-col flex-2 mt-6">
|
||||||
|
<ValidatedForm title="Athletics Information" on_submit=controller.submit_action>
|
||||||
|
<MultiEntry
|
||||||
|
label="Athletics Activities:"
|
||||||
|
description="Leave blank if not applicable."
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="sports_participation"
|
||||||
|
schema=vec![
|
||||||
|
input!(
|
||||||
|
Select, "sport_name", "Sport Name:", true, [
|
||||||
|
"Football",
|
||||||
|
"Soccer",
|
||||||
|
"Cheerleading",
|
||||||
|
"Field Hockey",
|
||||||
|
"Swimming",
|
||||||
|
"Golf",
|
||||||
|
"Basketball",
|
||||||
|
"Track",
|
||||||
|
"Gymnastics",
|
||||||
|
"Ice Hockey",
|
||||||
|
"Ski",
|
||||||
|
"Wrestling",
|
||||||
|
"Lacrosse",
|
||||||
|
"Softball",
|
||||||
|
"Tennis",
|
||||||
|
"Baseball"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
input!(
|
||||||
|
Checkbox, "grades", "Grades Participated:", true, [
|
||||||
|
"9th",
|
||||||
|
"10th",
|
||||||
|
"11th",
|
||||||
|
"12th"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
input!(
|
||||||
|
Text, "achievements", "Special Achievements:", false, "Example 1, Example 2, etc..."
|
||||||
|
),
|
||||||
|
]
|
||||||
|
/>
|
||||||
|
</ValidatedForm>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::{Loading, OutlinedTextField, Select, TextFieldType, ValidatedForm};
|
||||||
|
use crate::pages::student::form_setup::use_student_form;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentDemographicsPage() -> impl IntoView {
|
||||||
|
// This page should show a form that the students need to fill out.
|
||||||
|
// It's a simple form, really nothing more than a contact form.
|
||||||
|
// I'm going to test out creating this form without using a panel - we'll just use the page as
|
||||||
|
// it is with a specially spaced invisible div instead of a shaded container.
|
||||||
|
|
||||||
|
let controller = use_student_form("demographics");
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
<div class="flex flex-col flex-2 mt-6">
|
||||||
|
<ValidatedForm title="Student Demographic Form" on_submit=controller.submit_action>
|
||||||
|
// Inputs here.
|
||||||
|
<OutlinedTextField
|
||||||
|
label="First Name:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="first_name"
|
||||||
|
placeholder="John"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Last Name:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="last_name"
|
||||||
|
placeholder="Smith"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
// There must be a date of birth selection field here.
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Student ID Number:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="id_number"
|
||||||
|
placeholder="Your 7 digit number..."
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Preferred Email:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="email"
|
||||||
|
placeholder="me@example.com"
|
||||||
|
input_type=TextFieldType::Email(vec!["*".to_string()])
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Preferred Phone Number:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="phone_number"
|
||||||
|
placeholder="000-000-0000"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Street Address:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="street_address"
|
||||||
|
placeholder="234 Judd Rd"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Town:"
|
||||||
|
value_list=vec!["Southbury".to_string(), "Middlebury".to_string()]
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="town"
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
</ValidatedForm>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
use leptos::logging::debug_log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::common::{ComparisonData, ExpandableInfo, ValueType};
|
||||||
|
use crate::pages::api::{get_all_scholarship_info, get_comparison_info};
|
||||||
|
use crate::pages::api::students::get_all_student_data;
|
||||||
|
use crate::utils::get_user_claims;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentEligibilityPage() -> impl IntoView {
|
||||||
|
// This page will show all scholarships that the student is eligible for.
|
||||||
|
// The scholarships are obviously pulled using a readonly API, and we'll also need to resolve
|
||||||
|
// all of the relation IDs that are stored inside of it.
|
||||||
|
// Then, for each scholarship found, we'll check every relation and filter out scholarships that
|
||||||
|
// do not pass the check. We'll show some sort of simple list to indicate what scholarships passed
|
||||||
|
// the check, and another to indicate which ones didn't.
|
||||||
|
|
||||||
|
// My only reservation with showing the scholarships that aren't valid is that people will want
|
||||||
|
// to know why. That's hard information to keep track of right now.
|
||||||
|
|
||||||
|
let user_claims = get_user_claims();
|
||||||
|
let user_id = Memo::new(move |_| {
|
||||||
|
user_claims.get()
|
||||||
|
.map(|info| info.claims.subject.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
let refresh_trigger = Trigger::new();
|
||||||
|
|
||||||
|
let scholarships_resource = Resource::new(
|
||||||
|
move || refresh_trigger.track(),
|
||||||
|
async move |_| {
|
||||||
|
get_all_scholarship_info().await
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let relations_resource = Resource::new(
|
||||||
|
move || refresh_trigger.track(),
|
||||||
|
async move |_| {
|
||||||
|
get_comparison_info().await
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let student_resource = Resource::new(
|
||||||
|
move || (user_id.get().unwrap_or_default(), refresh_trigger.track()),
|
||||||
|
async move |(user_id, _)| {
|
||||||
|
get_all_student_data(user_id).await
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex-1" />
|
||||||
|
<div>
|
||||||
|
<Suspense fallback=move || {
|
||||||
|
view! { <div>"Checking eligibility..."</div> }
|
||||||
|
}>
|
||||||
|
{move || {
|
||||||
|
let Some(Ok(scholarships_list)) = scholarships_resource.get() else {
|
||||||
|
return view! {}.into_any();
|
||||||
|
};
|
||||||
|
let Some(Ok(relations_list)) = relations_resource.get() else {
|
||||||
|
return view! {}.into_any();
|
||||||
|
};
|
||||||
|
let Some(Ok(student_info)) = student_resource.get() else {
|
||||||
|
return view! {}.into_any();
|
||||||
|
};
|
||||||
|
|
||||||
|
let valid_list = scholarships_list.iter()
|
||||||
|
.filter_map(|scholarship| {
|
||||||
|
let scholarship_name = scholarship.data.get("name")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.as_string().ok().flatten().unwrap_or_default();
|
||||||
|
debug_log!("Checking scholarship: {}", scholarship_name);
|
||||||
|
|
||||||
|
// Each scholarship will return Some(itself), or None. This is will be automatically
|
||||||
|
// filtered based on the information. Invalid scholarships are None, along with those
|
||||||
|
// that fail validation for any reason. Scholarships that are valid or did not choose
|
||||||
|
// any requirements are Some(scholarship).
|
||||||
|
let requirements_map = scholarship.data.get("requirements")
|
||||||
|
.unwrap_or(&ValueType::Map(None))
|
||||||
|
.as_map().ok().flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.values().cloned()
|
||||||
|
.collect::<Vec<ValueType>>();
|
||||||
|
|
||||||
|
let resolved_requirements = requirements_map
|
||||||
|
.iter()
|
||||||
|
.map(|list_val| {
|
||||||
|
// We want to cast to a list, and then map the list to a new
|
||||||
|
// one that contains the correct ComparisonData.
|
||||||
|
let list = list_val.as_list().ok().flatten().unwrap_or_default();
|
||||||
|
list.iter().filter_map(|v| {
|
||||||
|
let id_string = v.as_string().ok().flatten().unwrap_or_default();
|
||||||
|
relations_list.iter()
|
||||||
|
.find(|relation| relation.id == id_string)
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<ComparisonData>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<ComparisonData>>>();
|
||||||
|
|
||||||
|
debug_log!("Resolved requirements: {:?}", resolved_requirements.iter().flatten().map(|rel| rel.display_text.clone()).collect::<Vec<String>>());
|
||||||
|
|
||||||
|
let valid = resolved_requirements.iter()
|
||||||
|
.all(|list| {
|
||||||
|
list.iter().fold(false, |prev, requirement| {
|
||||||
|
let result = requirement.compare(&student_info)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if prev { prev } else { result }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if valid {
|
||||||
|
debug_log!("Scholarship is valid.");
|
||||||
|
Some(scholarship.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<ExpandableInfo>>();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>{format!("Valid scholarships: {}", valid_list.len())}</div>
|
||||||
|
<For
|
||||||
|
each=move || valid_list.clone()
|
||||||
|
key=|scholarship| {
|
||||||
|
scholarship
|
||||||
|
.subject
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
children=move |scholarship| {
|
||||||
|
let scholarship = StoredValue::new(scholarship);
|
||||||
|
let scholarship_name = Memo::new(move |_| {
|
||||||
|
scholarship
|
||||||
|
.read_value()
|
||||||
|
.data
|
||||||
|
.get("name")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.as_string()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let scholarship_essay_prompt = Memo::new(move |_| {
|
||||||
|
scholarship
|
||||||
|
.read_value()
|
||||||
|
.data
|
||||||
|
.get("essay_prompt")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.as_string()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let essay_required = Memo::new(move |_| {
|
||||||
|
!scholarship_essay_prompt.get().is_empty()
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-row gap-2 p-3">
|
||||||
|
<div>{scholarship_name}</div>
|
||||||
|
<Show when=move || essay_required.get()>
|
||||||
|
<div>{scholarship_essay_prompt}</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}.into_any()
|
||||||
|
}}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1" />
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::{Loading, MultiEntry, OutlinedTextField, TextFieldType, ValidatedForm};
|
||||||
|
use crate::input;
|
||||||
|
use crate::pages::student::form_setup::use_student_form;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentExtracurricularsPage() -> impl IntoView {
|
||||||
|
let controller = use_student_form("demographics");
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
<div class="flex flex-col flex-2 mt-6">
|
||||||
|
<ValidatedForm
|
||||||
|
title="Extracurricular Information"
|
||||||
|
on_submit=controller.submit_action
|
||||||
|
>
|
||||||
|
// Inputs here.
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Total number of service hours:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="service_hours"
|
||||||
|
placeholder="Any number..."
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<MultiEntry
|
||||||
|
label="Extracurricular Activities:"
|
||||||
|
description="Leave blank if not applicable."
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="extracurricular"
|
||||||
|
schema=vec![
|
||||||
|
input!(
|
||||||
|
Text, "activity_name", "Activity Name:", true, "Some activity..."
|
||||||
|
),
|
||||||
|
input!(Number, "num_hours", "Number of hours completed:", true, "40"),
|
||||||
|
input!(Number, "num_weeks", "Number of weeks participated:", true, "3"),
|
||||||
|
input!(
|
||||||
|
Text, "special_involvement", "Any special involvement:", false, ""
|
||||||
|
),
|
||||||
|
input!(
|
||||||
|
Checkbox, "grades", "Grades Participated:", true, ["9th", "10th", "11th", "12th"]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
/>
|
||||||
|
</ValidatedForm>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::{Loading, OutlinedTextField, TextFieldType, ValidatedForm};
|
||||||
|
use crate::pages::student::form_setup::use_student_form;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentFamilyPage() -> impl IntoView {
|
||||||
|
let controller = use_student_form("family");
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
<div class="flex flex-col flex-2 mt-6">
|
||||||
|
<ValidatedForm title="Family Information" on_submit=controller.submit_action>
|
||||||
|
// Inputs here.
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Total number of children in family:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="num_children"
|
||||||
|
placeholder="Any number..."
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Total number of children currently attending college:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="num_children_college"
|
||||||
|
placeholder="Any number..."
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Parent/Guardian 1 Name:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="parent_one_name"
|
||||||
|
placeholder="John Smith"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
// TODO Could this be a select dropdown?
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Parent/Guardian 1 Relationship:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="parent_one_relationship"
|
||||||
|
placeholder="Mother/Father"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
// TODO Get a better example
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Parent/Guardian 1 Occupation:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="parent_one_occupation"
|
||||||
|
placeholder="Milkman"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Parent/Guardian 1 Employer:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="parent_one_employer"
|
||||||
|
placeholder="Example Employer"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Parent/Guardian 2 Name:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="parent_two_name"
|
||||||
|
placeholder="John Smith"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=false
|
||||||
|
/>
|
||||||
|
// TODO Could this be a select dropdown?
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Parent/Guardian 2 Relationship:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="parent_two_relationship"
|
||||||
|
placeholder="Mother/Father"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=false
|
||||||
|
/>
|
||||||
|
// TODO Get a better example
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Parent/Guardian 2 Occupation:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="parent_two_occupation"
|
||||||
|
placeholder="Milkman"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=false
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Parent/Guardian 2 Employer:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="parent_two_employer"
|
||||||
|
placeholder="Example Employer"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=false
|
||||||
|
/>
|
||||||
|
</ValidatedForm>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::common::ValueType;
|
||||||
|
use crate::pages::api::students::{get_student_data, PutStudentData};
|
||||||
|
use crate::utils::get_user_claims;
|
||||||
|
|
||||||
|
pub struct StudentFormInfo {
|
||||||
|
pub data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
|
pub user_id: Memo<Option<String>>,
|
||||||
|
pub submit_action: Callback<()>,
|
||||||
|
pub refresh_trigger: Trigger,
|
||||||
|
pub data_resource: Resource<Result<HashMap<String, ValueType>, ServerFnError>>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn use_student_form(
|
||||||
|
form_type: impl Into<Signal<String>>
|
||||||
|
) -> StudentFormInfo {
|
||||||
|
let form_type = form_type.into();
|
||||||
|
|
||||||
|
let user_claims = get_user_claims();
|
||||||
|
let user_id = Memo::new(move |_| {
|
||||||
|
user_claims.get()
|
||||||
|
.map(|info| info.claims.subject.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
let data_map = RwSignal::new(HashMap::new());
|
||||||
|
|
||||||
|
let refresh_trigger = Trigger::new();
|
||||||
|
let data_resource = Resource::new(
|
||||||
|
move || (user_id.get().unwrap_or_default(), form_type.get(), refresh_trigger.track()),
|
||||||
|
async move |(id, form_type, _)| get_student_data(
|
||||||
|
id,
|
||||||
|
form_type,
|
||||||
|
).await
|
||||||
|
);
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
data_resource.with(|map_opt| {
|
||||||
|
let Some(Ok(map)) = map_opt else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
data_map.set(map.to_owned());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let submit_action = ServerAction::<PutStudentData>::new();
|
||||||
|
let on_submit = move || {
|
||||||
|
submit_action.dispatch(PutStudentData {
|
||||||
|
subject: user_id.get().unwrap_or_default(),
|
||||||
|
data_type: form_type.get(),
|
||||||
|
data_map: data_map.get()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
StudentFormInfo {
|
||||||
|
data_map,
|
||||||
|
refresh_trigger,
|
||||||
|
data_resource,
|
||||||
|
user_id,
|
||||||
|
submit_action: on_submit.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use leptos::logging::log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::common::ValueType;
|
||||||
|
use crate::components::ActionButton;
|
||||||
|
use crate::pages::api::students::{GetStudentData, PutStudentData};
|
||||||
|
use crate::utils::get_user_claims;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentHomePage() -> impl IntoView {
|
||||||
|
let user_claims = get_user_claims();
|
||||||
|
let user_id = Memo::new(move |_| {
|
||||||
|
user_claims.get()
|
||||||
|
.map(|info| info.claims.subject.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
let create_student_data = ServerAction::<PutStudentData>::new();
|
||||||
|
let get_student_data = ServerAction::<GetStudentData>::new();
|
||||||
|
|
||||||
|
let on_click_student_test = move |_| {
|
||||||
|
create_student_data.dispatch(PutStudentData {
|
||||||
|
subject: user_id.get().unwrap_or_default(),
|
||||||
|
data_type: "TESTING".into(),
|
||||||
|
data_map: HashMap::from([
|
||||||
|
("testing_info".into(), ValueType::String(Some("hello!".to_string())))
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_click_get_student = move |_| {
|
||||||
|
get_student_data.dispatch(GetStudentData {
|
||||||
|
subject: user_id.get().unwrap_or_default(),
|
||||||
|
data_type: "TESTING".into()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
let Some(value) = get_student_data.value().get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
log!("{:?}", value);
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-1 flex-col gap-4 items-center m-4">
|
||||||
|
<div class="font-bold text-3xl">"Student Home Page"</div>
|
||||||
|
<div class="text-lg">
|
||||||
|
"This is just a placeholder for now. We'll show some redirect buttons."
|
||||||
|
</div>
|
||||||
|
<ActionButton on:click=on_click_student_test>"Test Put Student Function"</ActionButton>
|
||||||
|
<ActionButton on:click=on_click_get_student>"Test Get Student Function"</ActionButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
mod shell;
|
||||||
|
mod home_page;
|
||||||
|
mod demographics_page;
|
||||||
|
mod additional_page;
|
||||||
|
mod athletics_page;
|
||||||
|
mod academics_page;
|
||||||
|
mod extracurriculars_page;
|
||||||
|
mod work_experience_page;
|
||||||
|
mod university_page;
|
||||||
|
mod family_info_page;
|
||||||
|
mod form_setup;
|
||||||
|
mod eligibility_page;
|
||||||
|
|
||||||
|
pub use shell::*;
|
||||||
|
pub use home_page::*;
|
||||||
|
pub use demographics_page::*;
|
||||||
|
pub use additional_page::*;
|
||||||
|
pub use athletics_page::*;
|
||||||
|
pub use academics_page::*;
|
||||||
|
pub use extracurriculars_page::*;
|
||||||
|
pub use work_experience_page::*;
|
||||||
|
pub use university_page::*;
|
||||||
|
pub use family_info_page::*;
|
||||||
|
pub use eligibility_page::*;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||||
|
use crate::common::{SubTabInfo, TabInfo};
|
||||||
|
use crate::components::{Banner, Loading, TabSidebarList};
|
||||||
|
use crate::components::login::StudentLoginContext;
|
||||||
|
use crate::pages::UnauthenticatedPage;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentShell() -> impl IntoView {
|
||||||
|
// Create login context
|
||||||
|
view! {
|
||||||
|
<StudentLoginContext>
|
||||||
|
<div class="h-screen flex flex-col overflow-hidden">
|
||||||
|
<Banner
|
||||||
|
title="R15 Student Scholarship Application"
|
||||||
|
logo="/PHS_Stacked_Acronym.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-1 min-h-0">
|
||||||
|
<AuthLoaded fallback=Loading>
|
||||||
|
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||||
|
<TabSidebarList
|
||||||
|
base_path="students"
|
||||||
|
tabs=vec![
|
||||||
|
TabInfo::new("Home", "home", None),
|
||||||
|
TabInfo::new("Demographics", "demographics", None),
|
||||||
|
TabInfo::new(
|
||||||
|
"Additional Info",
|
||||||
|
"additional",
|
||||||
|
Some(
|
||||||
|
vec![
|
||||||
|
SubTabInfo::new("Academics", "academics"),
|
||||||
|
SubTabInfo::new("Athletics", "athletics"),
|
||||||
|
SubTabInfo::new("Work Experience", "work-experience"),
|
||||||
|
SubTabInfo::new("Extracurriculars", "extracurriculars"),
|
||||||
|
SubTabInfo::new("University Info", "university"),
|
||||||
|
SubTabInfo::new("Family Info", "family-info"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TabInfo::new("Eligible Scholarships", "eligibility", None)
|
||||||
|
]
|
||||||
|
/>
|
||||||
|
</Authenticated>
|
||||||
|
</AuthLoaded>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</StudentLoginContext>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::{Loading, OutlinedTextField, Select, TextFieldType, ValidatedForm};
|
||||||
|
use crate::pages::student::form_setup::use_student_form;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentUniversityPage() -> impl IntoView {
|
||||||
|
let controller = use_student_form("university");
|
||||||
|
|
||||||
|
// college name, state, city, zip
|
||||||
|
// major, study field, intended career, college_acceptance
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
<div class="flex flex-col flex-2 mt-6">
|
||||||
|
<ValidatedForm title="University Information" on_submit=controller.submit_action>
|
||||||
|
// Inputs here.
|
||||||
|
<OutlinedTextField
|
||||||
|
label="University Name:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="college_name"
|
||||||
|
placeholder="University of Example"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="University Street Address:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="college_city"
|
||||||
|
placeholder="123 Example Rd"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
// TODO Make this a select dropdown.
|
||||||
|
<OutlinedTextField
|
||||||
|
label="University State:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="college_state"
|
||||||
|
placeholder="The State of Panic"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="University ZIP:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="college_zip"
|
||||||
|
placeholder="12345"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
label="Have you been sent an acceptance to the university?"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="college_acceptance"
|
||||||
|
required=true
|
||||||
|
value_list=vec!["Yes".to_string(), "No".to_string()]
|
||||||
|
/>
|
||||||
|
// TODO Make this a select dropdown.
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Chosen Major:"
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="major"
|
||||||
|
placeholder="Computer Science"
|
||||||
|
input_type=TextFieldType::Text
|
||||||
|
required=true
|
||||||
|
/>
|
||||||
|
</ValidatedForm>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::{Loading, MultiEntry, ValidatedForm};
|
||||||
|
use crate::input;
|
||||||
|
use crate::pages::student::form_setup::use_student_form;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn StudentWorkExperiencePage() -> impl IntoView {
|
||||||
|
let controller = use_student_form("workexp");
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show when=move || controller.data_resource.get().is_some() fallback=Loading>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
<div class="flex flex-col flex-2 mt-6">
|
||||||
|
<ValidatedForm title="Student Work Experience" on_submit=controller.submit_action>
|
||||||
|
<MultiEntry
|
||||||
|
label="Work Experience:"
|
||||||
|
description="Leave blank if not applicable."
|
||||||
|
data_map=controller.data_map
|
||||||
|
data_member="extracurricular"
|
||||||
|
schema=vec![
|
||||||
|
input!(
|
||||||
|
Text, "job_title", "Job Title:", true, "Waiter, Cashier, etc..."
|
||||||
|
),
|
||||||
|
input!(Text, "employer", "Employer:", true, "Your employer's name..."),
|
||||||
|
input!(Number, "start_date", "Start date:", true, "40"),
|
||||||
|
input!(Number, "end_date", "End date:", true, "3"),
|
||||||
|
input!(Number, "num_hours", "Number of hours completed:", true, "120"),
|
||||||
|
]
|
||||||
|
/>
|
||||||
|
</ValidatedForm>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1" />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
use chrono::DateTime;
|
|
||||||
use crate::components::ActionButton;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use crate::common::{DateInfo, DateRange};
|
|
||||||
use crate::pages::api::{CreateDates, CreateTestComparisons};
|
|
||||||
|
|
||||||
fn get_important_dates() -> Vec<DateInfo> {
|
|
||||||
vec! [
|
|
||||||
DateInfo {
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
title: "Provider Forms Open".to_string(),
|
|
||||||
date: DateRange::Range(
|
|
||||||
DateTime::parse_from_rfc3339("2026-01-20T07:15:00-05:00").unwrap(),
|
|
||||||
DateTime::parse_from_rfc3339("2026-03-03T23:59:00-05:00").unwrap()
|
|
||||||
),
|
|
||||||
description: "".to_string(),
|
|
||||||
},
|
|
||||||
DateInfo {
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
title: "Student Forms Open".to_string(),
|
|
||||||
date: DateRange::Range(
|
|
||||||
DateTime::parse_from_rfc3339("2026-03-03T07:15:00-05:00").unwrap(),
|
|
||||||
DateTime::parse_from_rfc3339("2026-03-27T14:05:00-05:00").unwrap()
|
|
||||||
),
|
|
||||||
description: "".to_string(),
|
|
||||||
},
|
|
||||||
DateInfo {
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
title: "Scholarship Decision Window".to_string(),
|
|
||||||
date: DateRange::Range(
|
|
||||||
DateTime::parse_from_rfc3339("2026-04-10T07:15:00-05:00").unwrap(),
|
|
||||||
DateTime::parse_from_rfc3339("2026-05-01T23:59:00-05:00").unwrap()
|
|
||||||
),
|
|
||||||
description: "Start date may change if available sooner.".to_string(),
|
|
||||||
},
|
|
||||||
DateInfo {
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
title: "PHS Scholarship Committee".to_string(),
|
|
||||||
date: DateRange::Single(
|
|
||||||
DateTime::parse_from_rfc3339("2026-05-18T00:00:00-05:00").unwrap(),
|
|
||||||
),
|
|
||||||
description: "Time TBD".to_string(),
|
|
||||||
},
|
|
||||||
DateInfo {
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
title: "Scholarship and Awards Night".to_string(),
|
|
||||||
date: DateRange::Single(
|
|
||||||
DateTime::parse_from_rfc3339("2026-06-09T00:00:00-05:00").unwrap(),
|
|
||||||
),
|
|
||||||
description: "Time TBD".to_string(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn TestPage() -> impl IntoView {
|
|
||||||
let create_comparisons = ServerAction::<CreateTestComparisons>::new();
|
|
||||||
let create_dates = ServerAction::<CreateDates>::new();
|
|
||||||
|
|
||||||
let on_click_comparisons = move |_| {
|
|
||||||
create_comparisons.dispatch(CreateTestComparisons{});
|
|
||||||
};
|
|
||||||
|
|
||||||
let on_click_dates = move |_| {
|
|
||||||
create_dates.dispatch(CreateDates{dates: get_important_dates()});
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div class="mx-auto px-6">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<ActionButton on:click=on_click_comparisons>"Create Comparisons"</ActionButton>
|
|
||||||
<ActionButton on:click=on_click_dates>"Create Dates"</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}.into_any()
|
|
||||||
}
|
|
||||||
@@ -32,9 +32,9 @@ pub fn UnauthenticatedPage() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex h-screen justify-center items-center pb-50">
|
<div class="flex mx-auto 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 first."</h3>
|
||||||
<div class="flex pt-4 justify-center items-center">
|
<div class="flex pt-4 justify-center items-center">
|
||||||
<ActionButton on:click=on_click>"Sign in"</ActionButton>
|
<ActionButton on:click=on_click>"Sign in"</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ pub fn use_origin() -> String {
|
|||||||
/// Gets the current user claims. This function should only be used in an area that has
|
/// 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.
|
/// access to an AuthSignal, or it will result in a total failure.
|
||||||
pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> {
|
pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> {
|
||||||
let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal.");
|
let auth = expect_context::<AuthSignal>();
|
||||||
Signal::derive(move || {
|
Signal::derive(move || {
|
||||||
auth.with(|auth| {
|
auth.with(|auth| {
|
||||||
auth.authenticated().and_then(|data| {
|
auth.authenticated().and_then(|data| {
|
||||||
|
|||||||
Reference in new issue
Block a user