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_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
|
||||
use leptos_oidc::AuthSignal;
|
||||
@@ -61,8 +62,10 @@ pub fn AppWithRoutes() -> impl IntoView {
|
||||
<ToastList>
|
||||
// TODO Create a 404 page
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
// <Route path=path!("/testing") view=TestPage />
|
||||
<ProviderRoutes />
|
||||
<LoanerRoutes />
|
||||
<StudentRoutes />
|
||||
</Routes>
|
||||
</ToastList>
|
||||
</main>
|
||||
@@ -100,6 +103,53 @@ fn LoanerRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
.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]
|
||||
pub fn AuthErrorView() -> impl IntoView {
|
||||
let auth = use_context::<AuthSignal>().expect("AuthError: RwSignal<AuthStore> was not found");
|
||||
|
||||
@@ -2,7 +2,8 @@ use super::{
|
||||
Comparison, MapListComparison, NestedListComparison, NumberComparison, NumberListComparison,
|
||||
TextComparison, TextListComparison,
|
||||
};
|
||||
use crate::common::{ExpandableInfo, ValueType};
|
||||
use crate::common::ValueType;
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
@@ -42,15 +43,13 @@ impl ComparisonData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare(&self, student_data: &ExpandableInfo) -> Result<bool, String> {
|
||||
let value = student_data.data.get(&self.member).expect(
|
||||
format!(
|
||||
"Couldn't find member {:?}, current student data: {:?}",
|
||||
self.member, student_data
|
||||
pub fn compare(&self, student_data: &HashMap<String, ValueType>) -> Result<bool, String> {
|
||||
student_data.get(&self.member)
|
||||
.ok_or_else(||
|
||||
format!("Couldn't find member {:?}, current student data: {:?}", self.member, student_data)
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
self.comparison.evaluate(value, &self.target_value)
|
||||
.and_then(|value| self.comparison.evaluate(value, &self.target_value))
|
||||
.or_else(|err| Err(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,11 +105,11 @@ mod tests {
|
||||
display_text: "First Name is Jane".to_string(),
|
||||
};
|
||||
|
||||
let mut result = text_comp_success.compare(&student_data);
|
||||
let mut result = text_comp_success.compare(&student_data.data);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), true);
|
||||
|
||||
result = text_comp_fail.compare(&student_data);
|
||||
result = text_comp_fail.compare(&student_data.data);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,16 @@ impl Comparison for NumberComparison {
|
||||
// The prerequisites for all number comparisons is that the target value is Some(number) and the
|
||||
// comparison member points to Some(number) in the student_info.
|
||||
let student_value = match comp_value {
|
||||
ValueType::Number(Some(num)) => num.parse::<i32>().unwrap(),
|
||||
ValueType::Number(Some(num)) => num.parse::<f32>().unwrap(),
|
||||
_ => {
|
||||
return Err("Student value is not a number".into());
|
||||
}
|
||||
};
|
||||
|
||||
leptos::logging::log!("Parsing value: {:?}", target_value);
|
||||
|
||||
let target = match target_value {
|
||||
ValueType::Number(Some(num)) => num.parse::<i32>().unwrap(),
|
||||
ValueType::Number(Some(num)) => num.parse::<f32>().unwrap(),
|
||||
_ => {
|
||||
return Err("Target value is not a number".into());
|
||||
}
|
||||
|
||||
@@ -26,7 +26,14 @@ pub enum 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! {
|
||||
{match self {
|
||||
InputType::Text(member, label, placeholder, required) =>
|
||||
@@ -56,6 +63,7 @@ impl InputType {
|
||||
view! {
|
||||
<CheckboxList
|
||||
label = label.clone()
|
||||
name = name.clone()
|
||||
items = options.clone()
|
||||
data_member = member.clone()
|
||||
data_map = data_map
|
||||
|
||||
+2
-1
@@ -7,8 +7,9 @@ mod submit_status;
|
||||
mod user;
|
||||
mod value_type;
|
||||
pub mod animations;
|
||||
mod tab_info;
|
||||
|
||||
pub use self::{
|
||||
comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*,
|
||||
submit_status::*, user::*, value_type::*,
|
||||
submit_status::*, user::*, value_type::*, tab_info::*,
|
||||
};
|
||||
@@ -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,
|
||||
/// The file path or URL of a logo displayed at the top left of the banner.
|
||||
#[prop(into)]
|
||||
logo: String,
|
||||
logo: Signal<String>,
|
||||
/// The route that the navigates user to a page (home page).
|
||||
#[prop(into, optional)]
|
||||
path: Signal<String>,
|
||||
@@ -39,36 +39,26 @@ pub fn Banner(
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="mx-auto">
|
||||
<div class="bg-red-900 shadow-lg shadow-[inset_0_2px_4px_0_rgba(0,0,0,0.3),0_4px_6px_-1px_rgba(127,29,29,0.3),0_2px_4px_-1px_rgba(127,29,29,0.2)]">
|
||||
<div class="flex flex-row items-center gap-4 p-6 pb-2">
|
||||
<div class="flex-shrink-0">
|
||||
<img
|
||||
src=logo.clone()
|
||||
class="h-10 w-10 cursor-pointer"
|
||||
alt="icon"
|
||||
on:click=on_click
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h2 class="text-white text-2xl font-bold">{title.clone()}</h2>
|
||||
</div>
|
||||
<div class="w-screen bg-red-900">
|
||||
<div class="flex flex-row items-center gap-4 p-6 pb-2">
|
||||
<div class="flex-shrink-0">
|
||||
<img src=logo class="h-10 w-10 cursor-pointer" alt="icon" on:click=on_click />
|
||||
</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 class="flex-1">
|
||||
<h2 class="text-white text-2xl font-bold">{title.clone()}</h2>
|
||||
</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>
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,10 @@ pub fn Checkbox(
|
||||
/// ```
|
||||
#[component]
|
||||
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`.
|
||||
#[prop(into)] data_member: Signal<String>,
|
||||
/// A [`Signal`] allowing access to a data store.
|
||||
@@ -160,7 +164,7 @@ pub fn CheckboxList(
|
||||
on_change=on_change
|
||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||
value=item
|
||||
name=data_member
|
||||
name=move || format!("{}_{}", name.get(), data_member.get())
|
||||
disabled=disabled
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod utils;
|
||||
mod checkbox;
|
||||
mod chips;
|
||||
mod select_nav_list;
|
||||
|
||||
pub use checkbox::*;
|
||||
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)] required: Signal<bool>,
|
||||
#[prop(optional, into)] other_prompt: Signal<String>,
|
||||
#[prop(optional, into)] name: Signal<String>
|
||||
) -> impl IntoView {
|
||||
//#region Value Logic
|
||||
|
||||
@@ -262,7 +263,7 @@ pub fn RadioList(
|
||||
on_change=on_change
|
||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||
value=item
|
||||
name=data_member
|
||||
name=move || format!("{}_{}", name.get(), data_member.get())
|
||||
disabled=disabled
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use leptos::ev::{Event, Targeted};
|
||||
use leptos::prelude::*;
|
||||
use leptos::web_sys::HtmlSelectElement;
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::{log, warn};
|
||||
use crate::components::{use_validation_context, InputState, 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.
|
||||
#[component]
|
||||
pub fn Select(
|
||||
#[prop()] value_list: Vec<String>,
|
||||
#[prop(into)] value_list: Signal<Vec<String>>,
|
||||
#[prop(into)] data_member: Signal<String>,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop(optional, into)] label: String,
|
||||
@@ -29,27 +28,18 @@ pub fn Select(
|
||||
) -> impl IntoView {
|
||||
//#region Value Logic
|
||||
|
||||
let default_raw_value = data_map.get_untracked()
|
||||
.get(&data_member.get_untracked())
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.as_string().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())));
|
||||
});
|
||||
let value = Memo::new(move |_| {
|
||||
data_map.with(|map| {
|
||||
map.get(&data_member.get_untracked())
|
||||
.and_then(|v| v.as_string().ok().flatten())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
});
|
||||
|
||||
//#endregion
|
||||
//#region Validation Logic
|
||||
|
||||
let error = RwSignal::new(
|
||||
validate(required.get_untracked(), raw_value.get_untracked())
|
||||
);
|
||||
warn!("Current error state: {:?}", error.get_untracked());
|
||||
let error = Memo::new(move |_| validate(required.get(), value.get()));
|
||||
let dirty = RwSignal::new(false);
|
||||
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 value = e.target().value();
|
||||
log!("Found value {}", value);
|
||||
raw_value.set(value.clone());
|
||||
error.set(validate(required.get(), raw_value.get()));
|
||||
dirty.set(true);
|
||||
data_map.update(|map| {
|
||||
if let Some(val) = map.get_mut(&data_member.get()) {
|
||||
*val = ValueType::String(Some(value.clone()));
|
||||
} else {
|
||||
map.insert(data_member.get(), ValueType::String(Some(value)));
|
||||
}
|
||||
map.insert(data_member.get(), ValueType::String(Some(value)));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -96,27 +79,21 @@ pub fn Select(
|
||||
transition-border duration-150
|
||||
border-red-700 bg-transparent
|
||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
prop:value=move || value.get()
|
||||
on:change:target=on_change
|
||||
disabled=disabled
|
||||
>
|
||||
// This closure handles the display of the options.
|
||||
<option
|
||||
value=""
|
||||
disabled
|
||||
hidden
|
||||
selected=move || raw_value.get().is_empty()
|
||||
>
|
||||
<option value="" disabled hidden selected=move || value.get().is_empty()>
|
||||
"Select one..."
|
||||
</option>
|
||||
{move || {
|
||||
value_list
|
||||
.iter()
|
||||
.map(move |value| {
|
||||
let display = value.to_owned();
|
||||
view! { <option value=display>{display.clone()}</option> }
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}}
|
||||
|
||||
// We can reasonably assume that the options in the list will be distinct.
|
||||
<For
|
||||
each=move || value_list.get()
|
||||
key=|value| value.clone()
|
||||
children=move |val| view! { <option value=val>{val.clone()}</option> }
|
||||
/>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ use leptos::prelude::*;
|
||||
#[component]
|
||||
pub fn Loading() -> impl IntoView {
|
||||
view! {
|
||||
<div class="size-20 mx-auto">
|
||||
<div class="size-20 mx-auto mt-20">
|
||||
<img src="/loading.gif" />
|
||||
</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;
|
||||
pub use provider::*;
|
||||
mod student;
|
||||
pub use student::*;
|
||||
pub(crate) mod setup;
|
||||
mod admin;
|
||||
pub use admin::*;
|
||||
@@ -1,5 +1,4 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::Outlet;
|
||||
use super::setup::provide_auth_context;
|
||||
|
||||
///# Student Login Context Component
|
||||
@@ -16,7 +15,9 @@ use super::setup::provide_auth_context;
|
||||
/// </StudentLoginContext>
|
||||
/// }
|
||||
#[component]
|
||||
pub fn StudentLoginContext() -> impl IntoView {
|
||||
pub fn StudentLoginContext(
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
provide_auth_context(
|
||||
crate::utils::use_origin(),
|
||||
"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM",
|
||||
@@ -24,5 +25,5 @@ pub fn StudentLoginContext() -> impl IntoView {
|
||||
"/students/callback"
|
||||
);
|
||||
|
||||
view! { <Outlet /> }
|
||||
view! { {children()} }
|
||||
}
|
||||
@@ -13,9 +13,10 @@ mod lists;
|
||||
mod toasts;
|
||||
mod header;
|
||||
pub mod login;
|
||||
mod tabs;
|
||||
|
||||
pub use self::{
|
||||
action_button::*, banner::*, dashboard_button::*, lists::*,
|
||||
loading::*, multi_entry::*, panel::*, row::*, text_field::*,
|
||||
validated_form::*, toasts::*, header::*, date::*
|
||||
validated_form::*, toasts::*, header::*, date::*, tabs::*
|
||||
};
|
||||
+112
-66
@@ -1,8 +1,9 @@
|
||||
use crate::common::{InputType, ValueType};
|
||||
use crate::components::ActionButton;
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::log;
|
||||
use leptos_icons::Icon;
|
||||
use uuid::Uuid;
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
fn update_entry_list(list: &mut Vec<ValueType>, data: &HashMap<String, ValueType>) {
|
||||
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`.
|
||||
#[component]
|
||||
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`.
|
||||
#[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
|
||||
/// component's `data_member` input.
|
||||
#[prop()]
|
||||
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
/// The type and order of the inputs for each `Entry` component within this `MultiEntry` component.
|
||||
#[prop(optional)]
|
||||
schema: Vec<InputType>,
|
||||
#[prop(optional, into)]
|
||||
schema: Signal<Vec<InputType>>,
|
||||
) -> impl IntoView {
|
||||
// Make the values within the list reactive. Each of these values should be a ValueType::Map,
|
||||
// so we can pass these directly to the application.
|
||||
let data_reactive = RwSignal::new(
|
||||
data_map
|
||||
.get_untracked()
|
||||
.get(&data_member)
|
||||
.unwrap_or(&ValueType::List(None))
|
||||
.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 data_list = Memo::new(move |_| {
|
||||
data_map.with(|map| {
|
||||
map.get(&data_member.get())
|
||||
.and_then(|v| v.as_list().ok().flatten())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
});
|
||||
|
||||
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! {
|
||||
<div class="flex flex-col flex-1">
|
||||
<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
|
||||
<For
|
||||
each=move || data_reactive.get()
|
||||
key=|entry| get_uuid_from_map(&entry.as_map().unwrap().unwrap()) // Entries MUST be given a unique ID. This is absolutely ridiculous.
|
||||
children=move |mut entry_map| {
|
||||
// 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()
|
||||
/>
|
||||
}
|
||||
<Show
|
||||
when=move || !data_list.get().is_empty()
|
||||
fallback=|| {
|
||||
view! { <div class="mx-auto">"You haven't added any entries yet."</div> }
|
||||
}
|
||||
/>
|
||||
>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<ActionButton on:click=add_entry>"Add entry"</ActionButton>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -176,20 +207,35 @@ fn Entry(
|
||||
/// 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
|
||||
/// passed down from within the entry list in the parent `MultiEntry` component.
|
||||
#[prop()]
|
||||
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
/// The schema of this entry. Should contain all information about what inputs to use and what
|
||||
/// data members those inputs modify.
|
||||
#[prop()]
|
||||
schema: Vec<InputType>,
|
||||
#[prop(into)] schema: Signal<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 {
|
||||
view! {
|
||||
<div class="flex flex-col flex-1 p-2 rounded-sm transition-shadow shadow-sm hover:shadow-lg/30">
|
||||
{schema.iter()
|
||||
.map(|input_type| {
|
||||
input_type.clone().into_view(data_map.clone())
|
||||
}).collect_view()
|
||||
}
|
||||
<div class="relative rounded-sm transition-shadow shadow-sm hover:shadow-lg/30">
|
||||
// Delete button
|
||||
<div
|
||||
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"
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum ValidationState {
|
||||
/// A valid input.
|
||||
#[default]
|
||||
@@ -149,15 +150,8 @@ pub fn ValidatedForm(
|
||||
};
|
||||
|
||||
view! {
|
||||
<form
|
||||
class="flex flex-col gap-2"
|
||||
novalidate
|
||||
on:submit=submit_success_event
|
||||
>
|
||||
<Header
|
||||
title=title
|
||||
description=description
|
||||
/>
|
||||
<form class="flex flex-col gap-2 py-7" novalidate on:submit=submit_success_event>
|
||||
<Header title=title description=description />
|
||||
{children()}
|
||||
<ActionButton
|
||||
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")]
|
||||
use leptos::logging::log;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use super::COMPARISONS_TABLE;
|
||||
|
||||
use crate::common::{
|
||||
ComparisonData, ComparisonType, MapListComparison, NumberComparison, NumberListComparison,
|
||||
TextComparison, TextListComparison, ValueType,
|
||||
@@ -15,8 +18,6 @@ use std::collections::HashMap;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use leptos::server;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static COMPARISONS_TABLE: &str = "leptos-comparisons";
|
||||
|
||||
/// Helper function to create all sports comparisons. Only used to create initial
|
||||
/// comparison data, and should be removed upon completion of a comparison editor page.
|
||||
@@ -188,6 +189,17 @@ fn create_sports_comparisons() -> Vec<ComparisonData> {
|
||||
"Sports Participation",
|
||||
"Tennis",
|
||||
),
|
||||
ComparisonData::new(
|
||||
"sports_baseball",
|
||||
"sports_participation",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToTextList(
|
||||
"sport_name".to_string(),
|
||||
Box::new(TextListComparison::Contains),
|
||||
)),
|
||||
ValueType::String(Some("Baseball".to_string())),
|
||||
"Sports Participation",
|
||||
"Baseball",
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -252,12 +264,12 @@ fn create_major_comparisons() -> Vec<ComparisonData> {
|
||||
#[allow(unused)]
|
||||
fn make_comp_list() -> Vec<ComparisonData> {
|
||||
let gpa_3 = ComparisonData::new(
|
||||
"gpa_3",
|
||||
"gpa_top_third",
|
||||
"weighted_gpa",
|
||||
ComparisonType::Number(NumberComparison::GreaterThanOrEqual),
|
||||
ValueType::Number(Some(3.0.to_string())),
|
||||
ValueType::Number(Some(6.264.to_string())),
|
||||
"GPA Limits",
|
||||
"GPA >= 3.0",
|
||||
"Top Third GPA",
|
||||
);
|
||||
|
||||
let test_comp = ComparisonData::new(
|
||||
@@ -271,13 +283,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
|
||||
let service_hours_20 = ComparisonData::new(
|
||||
"service_hours_20",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
"service_hours",
|
||||
ComparisonType::Number(
|
||||
NumberComparison::GreaterThanOrEqual
|
||||
),
|
||||
ValueType::Number(Some(20.to_string())),
|
||||
"Community Service",
|
||||
"20+ service hours",
|
||||
@@ -285,13 +294,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
|
||||
let service_hours_25 = ComparisonData::new(
|
||||
"service_hours_25",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
"service_hours",
|
||||
ComparisonType::Number(
|
||||
NumberComparison::GreaterThanOrEqual
|
||||
),
|
||||
ValueType::Number(Some(25.to_string())),
|
||||
"Community Service",
|
||||
"25+ service hours",
|
||||
@@ -299,13 +305,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
|
||||
let service_hours_30 = ComparisonData::new(
|
||||
"service_hours_30",
|
||||
"community_involvement",
|
||||
ComparisonType::MapList(MapListComparison::FlattenToNumberList(
|
||||
"service_hours".to_string(),
|
||||
Box::new(NumberListComparison::Sum(Box::new(
|
||||
NumberComparison::GreaterThanOrEqual,
|
||||
))),
|
||||
)),
|
||||
"service_hours",
|
||||
ComparisonType::Number(
|
||||
NumberComparison::GreaterThanOrEqual
|
||||
),
|
||||
ValueType::Number(Some(30.to_string())),
|
||||
"Community Service",
|
||||
"30+ service hours",
|
||||
@@ -356,6 +359,42 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
"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 major_comps = create_major_comparisons();
|
||||
|
||||
@@ -370,6 +409,10 @@ fn make_comp_list() -> Vec<ComparisonData> {
|
||||
attended_bas,
|
||||
midd_south,
|
||||
family_military,
|
||||
youth_baseball,
|
||||
aquatic_club,
|
||||
pea_member,
|
||||
music_program
|
||||
];
|
||||
|
||||
comp_list.append(&mut sports_comps);
|
||||
|
||||
@@ -4,7 +4,7 @@ use leptos::prelude::*;
|
||||
use crate::common::DateInfo;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static DATES_TABLE: &str = "leptos-dates";
|
||||
use super::DATES_TABLE;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::{
|
||||
|
||||
+23
-3
@@ -1,7 +1,27 @@
|
||||
mod comparisons;
|
||||
pub use comparisons::*;
|
||||
mod scholarships;
|
||||
pub use scholarships::*;
|
||||
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")]
|
||||
use aws_sdk_dynamodb::{
|
||||
types::AttributeValue,
|
||||
error::ProvideErrorMetadata,
|
||||
};
|
||||
#[cfg(feature = "ssr")]
|
||||
use leptos::leptos_dom::log;
|
||||
#[cfg(feature = "ssr")]
|
||||
use uuid::Uuid;
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::utils::server::{create_dynamo_client, into_attr_map};
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::common::ValueType;
|
||||
mod imports {
|
||||
pub use leptos::logging::debug_log;
|
||||
pub use aws_sdk_dynamodb::{
|
||||
types::AttributeValue,
|
||||
error::ProvideErrorMetadata,
|
||||
};
|
||||
pub use leptos::leptos_dom::log;
|
||||
pub use uuid::Uuid;
|
||||
pub use crate::utils::server::{create_dynamo_client};
|
||||
pub use crate::common::ValueType;
|
||||
pub use super::super::SCHOLARSHIPS_TABLE;
|
||||
}
|
||||
|
||||
use crate::common::ExpandableInfo;
|
||||
|
||||
use leptos::prelude::ServerFnError;
|
||||
use leptos::server;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static SCHOLARSHIPS_TABLE: &str = "leptos-scholarships";
|
||||
use leptos::server_fn::codec::Json;
|
||||
|
||||
#[server(GetScholarshipInfo)]
|
||||
pub async fn get_scholarship_info(id: String) -> Result<ExpandableInfo, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
|
||||
// Perform the operation - we just want to return all data that's contained in this entry,
|
||||
@@ -50,8 +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> {
|
||||
use imports::*;
|
||||
use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
@@ -75,8 +75,10 @@ pub async fn create_scholarship_info(info: ExpandableInfo) -> Result<(), ServerF
|
||||
|
||||
#[server(GetAllScholarshipInfo)]
|
||||
pub async fn get_all_scholarship_info() -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
log!("Getting all scholarship info");
|
||||
debug_log!("Getting all scholarship info");
|
||||
match client
|
||||
.scan()
|
||||
.table_name(SCHOLARSHIPS_TABLE)
|
||||
@@ -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.
|
||||
#[server(GetProviderScholarships)]
|
||||
pub async fn get_provider_scholarships(provider_id: String) -> Result<Vec<ExpandableInfo>, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
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
|
||||
.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.
|
||||
#[server(RegisterScholarship)]
|
||||
pub async fn register_scholarship(provider_id: String) -> Result<String, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
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 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.
|
||||
#[server(DeleteProviderScholarship)]
|
||||
pub async fn delete_provider_scholarship(provider_id: String, scholarship_id: String) -> Result<(), ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
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,
|
||||
// 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,
|
||||
};
|
||||
let comparison_outcome = match comp_opt {
|
||||
Some(comp) => comp.compare(&student),
|
||||
Some(comp) => comp.compare(&student.data),
|
||||
_ => Err("".to_string()),
|
||||
};
|
||||
match comparison_outcome {
|
||||
|
||||
+2
-14
@@ -12,7 +12,7 @@ use crate::common::ExpandableInfo;
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
use crate::components::{
|
||||
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
|
||||
Select, TextFieldType, ValidatedForm
|
||||
TextFieldType, ValidatedForm
|
||||
};
|
||||
use crate::input;
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
@@ -329,21 +329,9 @@ pub fn HomePage() -> impl IntoView {
|
||||
required=true
|
||||
/>
|
||||
</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>
|
||||
<MultiEntry
|
||||
label="Testing community involvement:"
|
||||
data_map=form_data
|
||||
data_member="community_involvement"
|
||||
schema=vec![
|
||||
|
||||
+4
-3
@@ -5,14 +5,15 @@ mod home_page;
|
||||
mod loaner_page;
|
||||
mod provider_portal;
|
||||
mod scholarship_info;
|
||||
mod test_page;
|
||||
mod unauthenticated_page;
|
||||
mod provider_contact;
|
||||
mod auth_callback;
|
||||
mod provider_applicants;
|
||||
mod admin;
|
||||
pub(crate) mod student;
|
||||
|
||||
pub use self::{
|
||||
about_page::*, comparison_test::*, home_page::*, loaner_page::*, provider_portal::*,
|
||||
test_page::*, unauthenticated_page::*, scholarship_info::*, provider_contact::*,
|
||||
auth_callback::*, provider_applicants::*
|
||||
unauthenticated_page::*, scholarship_info::*, provider_contact::*,
|
||||
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">
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,77 +1,11 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::utils::server::{create_dynamo_client, into_attr_map};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashMap;
|
||||
use leptos::logging::log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use crate::common::ValueType;
|
||||
use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
||||
use crate::components::{Banner, Loading, OutlinedTextField, Panel, Row, TextFieldType, Toast, ToastContext, ValidatedForm};
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
use crate::utils::get_user_claims;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
static PROVIDER_CONTACT_TABLE: &str = "leptos-provider-contacts";
|
||||
|
||||
#[server]
|
||||
async fn get_provider_contact(id: String) -> Result<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))
|
||||
}
|
||||
}
|
||||
}
|
||||
use super::api::{get_provider_contact, PutProviderContact};
|
||||
|
||||
#[component]
|
||||
pub fn ProviderContactPage() -> impl IntoView {
|
||||
@@ -186,7 +120,7 @@ pub fn ProviderContactPage() -> impl IntoView {
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
placeholder="example@mymailservice.com"
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=contact_info
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::either::Either;
|
||||
use leptos::html::Dialog;
|
||||
use leptos::logging::log;
|
||||
use leptos::logging::debug_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use leptos_router::hooks::{use_navigate, use_params};
|
||||
@@ -111,7 +111,7 @@ fn ScholarshipList(
|
||||
(refresh_token.get(), provider_id.get())
|
||||
},
|
||||
|(_, provider_id)| async move {
|
||||
log!("Fetching from scholarship API.");
|
||||
debug_log!("Fetching from scholarship API.");
|
||||
let Some(provider_id) = provider_id else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
@@ -179,7 +179,7 @@ fn ScholarshipList(
|
||||
.get()
|
||||
.map(|s| s.subject.clone())
|
||||
.unwrap_or_default();
|
||||
log!("Deleting scholarship with subject {}", subject);
|
||||
debug_log!("Deleting scholarship with subject {}", subject);
|
||||
delete_action
|
||||
.dispatch(DeleteProviderScholarship {
|
||||
scholarship_id: subject,
|
||||
@@ -385,10 +385,10 @@ fn ScholarshipForm(
|
||||
if let Some(Ok(scholarship)) = scholarship_info.get() {
|
||||
// Get the default chips data.
|
||||
let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") {
|
||||
log!("Found requirements map: {:?}", map);
|
||||
debug_log!("Found requirements map: {:?}", map);
|
||||
map.clone()
|
||||
} else {
|
||||
log!("Couldn't find requirements map in scholarship info. Using empty map.");
|
||||
debug_log!("Couldn't find requirements map in scholarship info. Using empty map.");
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
@@ -496,7 +496,7 @@ fn ScholarshipForm(
|
||||
info.data = form_data.get();
|
||||
info.data.insert("requirements".to_string(), ValueType::Map(Some(chips_data.get())));
|
||||
|
||||
log!("Map values: {:?}", info.data);
|
||||
debug_log!("Map values: {:?}", info.data);
|
||||
submit_action.dispatch(CreateScholarshipInfo {
|
||||
info
|
||||
});
|
||||
@@ -505,7 +505,7 @@ fn ScholarshipForm(
|
||||
Effect::watch(
|
||||
move || submit_action.value().get(),
|
||||
move |value, _, _| {
|
||||
log!("Running submit_action effect");
|
||||
debug_log!("Running submit_action effect");
|
||||
let Some(result) = value else {
|
||||
return;
|
||||
};
|
||||
@@ -639,7 +639,7 @@ fn ScholarshipForm(
|
||||
<Header
|
||||
title="Eligibility 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>
|
||||
<ChipsList
|
||||
@@ -653,7 +653,7 @@ fn ScholarshipForm(
|
||||
</Row>
|
||||
<Row>
|
||||
<ChipsList
|
||||
label="Sports Participation"
|
||||
label="Sports and Activities"
|
||||
data_member="sports_participation"
|
||||
data_map=chips_data
|
||||
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! {
|
||||
<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">
|
||||
<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">
|
||||
<ActionButton on:click=on_click>"Sign in"</ActionButton>
|
||||
</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
|
||||
/// access to an AuthSignal, or it will result in a total failure.
|
||||
pub fn get_user_claims() -> Signal<Option<TokenData<UserClaims>>> {
|
||||
let auth = use_context::<AuthSignal>().expect("Couldn't find AuthSignal.");
|
||||
let auth = expect_context::<AuthSignal>();
|
||||
Signal::derive(move || {
|
||||
auth.with(|auth| {
|
||||
auth.authenticated().and_then(|data| {
|
||||
|
||||
Reference in new issue
Block a user