Implement template student portal #62

Merged
BobdaFett merged 21 commits from feat/value-display into main 2026-03-02 21:38:31 +00:00
3 changed files with 202 additions and 171 deletions
Showing only changes of commit c850ac208f - Show all commits

No files matched your search

+144 -98
View File
@@ -1,8 +1,8 @@
use crate::common::{SchemaNode, SchemaType, ValueType}; use crate::common::{SchemaNode, SchemaType, ValueType};
use leptos::prelude::*;
use std::collections::HashMap;
use indexmap::IndexMap; use indexmap::IndexMap;
use leptos::logging::debug_log; use leptos::logging::debug_log;
use leptos::prelude::*;
use std::collections::HashMap;
#[component] #[component]
pub fn ValueDisplay( pub fn ValueDisplay(
@@ -14,7 +14,8 @@ pub fn ValueDisplay(
if data_member.get().is_empty() { if data_member.get().is_empty() {
ValueType::Map(Some(data_map.get())) ValueType::Map(Some(data_map.get()))
} else { } else {
data_map.get() data_map
.get()
.get(&data_member.get()) .get(&data_member.get())
.map(|v| v.clone()) .map(|v| v.clone())
.unwrap_or_default() .unwrap_or_default()
@@ -24,55 +25,101 @@ pub fn ValueDisplay(
view! { view! {
{move || { {move || {
match schema.get().data_type { match schema.get().data_type {
SchemaType::String | SchemaType::Number => view! { SchemaType::String | SchemaType::Number => {
<PrimitiveDisplay view! {
header={move || schema.get().header} <PrimitiveDisplay
value=data_value header=move || schema.get().header
/> value=data_value
}.into_any(), nested=true
SchemaType::List => view! { />
<ListDisplay }
header=move || schema.get().header .into_any()
value=data_value }
item_template=move || schema.get().item_template.unwrap_or(Box::from(SchemaNode::new(SchemaType::List))) SchemaType::List => {
/> view! {
}.into_any(), <ListDisplay
SchemaType::Map => view! { header=move || schema.get().header
<MapDisplay value=data_value
value=data_value item_template=move || {
header=move || schema.get().header schema
schema=move || schema.get().children .get()
/> .item_template
}.into_any() .unwrap_or(Box::from(SchemaNode::new(SchemaType::List)))
}
nested=true
/>
}
.into_any()
}
SchemaType::Map => {
view! {
<MapDisplay
value=data_value
header=move || schema.get().header
schema=move || schema.get().children
nested=true
/>
}
.into_any()
}
} }
}} }}
} }
} }
#[component]
fn DisplayWrapper(
#[prop(into, optional)] header: Signal<String>,
#[prop(into, optional)] nested: Signal<bool>,
children: Children,
) -> impl IntoView {
// This component is the wrapper that is used in the rest of the display components.
// It allows for simpler styling of the header and capsule, along with simple collapsable
// functionality.
let expanded = RwSignal::new(true);
view! {
<div class="m-3 rounded-lg shadow-lg flex flex-col">
<div
class="p-2 px-3 font-bold rounded-t-lg cursor-pointer"
class=(["bg-gray-300"], move || !nested.get())
class=(["text-white", "bg-red-900"], move || nested.get())
class=(["rounded-lg"], move || !expanded.get())
on:click=move |_| expanded.update(|b| *b = !*b)
>
{header}
</div>
<div
class="grid transition-all ease-in-out"
style:grid-template-rows=move || if expanded.get() { "1fr" } else { "0fr" }
>
<div class="min-h-0 overflow-hidden">
<div class="p-2">{children()}</div>
</div>
</div>
</div>
}
}
#[component] #[component]
fn PrimitiveDisplay( fn PrimitiveDisplay(
#[prop(into)] header: Signal<String>, #[prop(into)] header: Signal<String>,
#[prop(into)] value: Signal<ValueType> #[prop(into)] value: Signal<ValueType>,
#[prop(into, optional)] nested: Signal<bool>,
) -> impl IntoView { ) -> impl IntoView {
let display_string = Memo::new(move |_| { let display_string = Memo::new(move |_| {
let value = value.get(); let value = value.get();
match value { match value {
ValueType::String(_) | ValueType::Number(_) => { ValueType::String(_) | ValueType::Number(_) => value.to_string(),
value.to_string() _ => "Value is not a string.".to_string(),
}
_ => {
"Value is not a string.".to_string()
}
} }
}); });
view! { view! {
<div> <DisplayWrapper header=header nested=nested>
<Show when=move || !header.get().is_empty()>
<div class="font-bold">{header}</div>
</Show>
<div>{display_string}</div> <div>{display_string}</div>
</div> </DisplayWrapper>
} }
} }
@@ -81,6 +128,7 @@ fn ListDisplay(
#[prop(into)] item_template: Signal<Box<SchemaNode>>, #[prop(into)] item_template: Signal<Box<SchemaNode>>,
#[prop(into)] value: Signal<ValueType>, #[prop(into)] value: Signal<ValueType>,
#[prop(into)] header: Signal<String>, #[prop(into)] header: Signal<String>,
#[prop(into, optional)] nested: Signal<bool>,
) -> impl IntoView { ) -> impl IntoView {
// Unwrap the value and check if it's the correct type. To be displayed with this component, it // Unwrap the value and check if it's the correct type. To be displayed with this component, it
// MUST be a ValueType::List. // MUST be a ValueType::List.
@@ -89,45 +137,46 @@ fn ListDisplay(
return Vec::new(); return Vec::new();
}; };
list.into_iter().enumerate().collect::<Vec<(usize, ValueType)>>() list.into_iter()
.enumerate()
.collect::<Vec<(usize, ValueType)>>()
}); });
let is_valid = Memo::new(move |_| { let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::List(_)));
matches!(value.get(), ValueType::List(_))
});
let item_header = Memo::new(move |_| { let item_header = Memo::new(move |_| item_template.get().header);
item_template.get().header
});
view! { view! {
<div> <DisplayWrapper header=header nested=nested>
<div class="font-bold">{header}</div>
<For <For
each=move || display_list.get() each=move || display_list.get()
key=|(idx, _)| idx.clone() key=|(idx, _)| idx.clone()
children=move |(idx, entry)| { children=move |(idx, entry)| {
match item_template.get().data_type { match item_template.get().data_type {
SchemaType::String | SchemaType::Number =>view! { SchemaType::String | SchemaType::Number => {
<PrimitiveDisplay view! { <PrimitiveDisplay value=entry header=item_header /> }.into_any()
value=entry }
header=item_header SchemaType::List => {
/> view! {
}.into_any(), <div>"Nested lists are not currently supported for display."</div>
SchemaType::List => view! { }
<div>"Nested lists are not currently supported for display."</div> .into_any()
}.into_any(), }
SchemaType::Map => view! { SchemaType::Map => {
<MapDisplay let map_header = format!("{} {}", item_template.get().header, idx + 1);
value=entry view! {
header=item_header <MapDisplay
schema=move || item_template.get().children value=entry
/> header=map_header
}.into_any() schema=move || item_template.get().children
/>
}
.into_any()
}
} }
} }
/> />
</div> </DisplayWrapper>
} }
} }
@@ -136,6 +185,7 @@ fn MapDisplay(
#[prop(into)] value: Signal<ValueType>, #[prop(into)] value: Signal<ValueType>,
#[prop(into)] header: Signal<String>, #[prop(into)] header: Signal<String>,
#[prop(into)] schema: Signal<IndexMap<String, SchemaNode>>, #[prop(into)] schema: Signal<IndexMap<String, SchemaNode>>,
#[prop(into, optional)] nested: Signal<bool>,
) -> impl IntoView { ) -> impl IntoView {
// Unwrap the value and check if it is the correct type. It must be a ValueType::Map. // Unwrap the value and check if it is the correct type. It must be a ValueType::Map.
let display_map = Memo::new(move |_| { let display_map = Memo::new(move |_| {
@@ -150,55 +200,51 @@ fn MapDisplay(
debug_log!("Current map values: {:?}", display_map.get()); debug_log!("Current map values: {:?}", display_map.get());
}); });
let is_valid = Memo::new(move |_| { let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::Map(_)));
matches!(value.get(), ValueType::Map(_))
});
view! { view! {
<div class="flex flex-col flex-1 gap-2"> <DisplayWrapper header=header nested=nested>
<div class="font-bold">{header}</div> <For
<div class="p-2 rounded-md shadow-lg/33 m-2"> each=move || schema.get()
<For key=|(k, _)| k.clone()
each=move || schema.get() children=move |(member, child_schema)| {
key=|(k, _)| k.clone() let display_value = Memo::new(move |_| {
children=move |(member, child_schema)| { display_map.get().get(&member).map(|v| v.clone()).unwrap_or_default()
let display_value = Memo::new(move |_| { });
display_map.get() match child_schema.data_type {
.get(&member) SchemaType::String | SchemaType::Number => {
.map(|v| v.clone()) view! {
.unwrap_or_default() <PrimitiveDisplay value=display_value header=child_schema.header />
}); }
.into_any()
}
SchemaType::List => {
let template = child_schema
.item_template
.unwrap_or(Box::from(SchemaNode::new(SchemaType::List)));
match child_schema.data_type { view! {
SchemaType::String | SchemaType::Number => view! { <ListDisplay
<PrimitiveDisplay
value=display_value value=display_value
item_template=template
header=child_schema.header header=child_schema.header
/> />
}.into_any(), }
SchemaType::List => { .into_any()
let template = child_schema.item_template }
.unwrap_or(Box::from(SchemaNode::new(SchemaType::List))); SchemaType::Map => {
view! {
view! {
<ListDisplay
value=display_value
item_template=template
header=child_schema.header
/>
}
}.into_any(),
SchemaType::Map => view! {
<MapDisplay <MapDisplay
value=display_value value=display_value
schema=child_schema.children schema=child_schema.children
header=child_schema.header header=child_schema.header
/> />
}.into_any() }
.into_any()
} }
} }
/> }
</div> />
</div> </DisplayWrapper>
} }
} }
+29 -62
View File
@@ -1,9 +1,10 @@
use crate::common::{ComparisonData, ExpandableInfo, SchemaNode, SchemaType, ValueType};
use crate::components::ValueDisplay;
use crate::pages::api::students::get_all_student_data;
use crate::pages::api::{get_all_scholarship_info, get_comparison_info};
use crate::utils::get_user_claims;
use leptos::logging::debug_log; use leptos::logging::debug_log;
use leptos::prelude::*; 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] #[component]
pub fn StudentEligibilityPage() -> impl IntoView { pub fn StudentEligibilityPage() -> impl IntoView {
@@ -13,37 +14,28 @@ pub fn StudentEligibilityPage() -> impl IntoView {
// Then, for each scholarship found, we'll check every relation and filter out scholarships that // 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 // 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. // 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 // 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. // to know why. That's hard information to keep track of right now.
let user_claims = get_user_claims(); let user_claims = get_user_claims();
let user_id = Memo::new(move |_| { let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
user_claims.get()
.map(|info| info.claims.subject.clone())
});
let refresh_trigger = Trigger::new(); let refresh_trigger = Trigger::new();
let scholarships_resource = Resource::new( let scholarships_resource = Resource::new(
move || refresh_trigger.track(), move || refresh_trigger.track(),
async move |_| { async move |_| get_all_scholarship_info().await,
get_all_scholarship_info().await
}
); );
let relations_resource = Resource::new( let relations_resource = Resource::new(
move || refresh_trigger.track(), move || refresh_trigger.track(),
async move |_| { async move |_| get_comparison_info().await,
get_comparison_info().await
}
); );
let student_resource = Resource::new( let student_resource = Resource::new(
move || (user_id.get().unwrap_or_default(), refresh_trigger.track()), move || (user_id.get().unwrap_or_default(), refresh_trigger.track()),
async move |(user_id, _)| { async move |(user_id, _)| get_all_student_data(user_id).await,
get_all_student_data(user_id).await
}
); );
view! { view! {
<div class="flex-1" /> <div class="flex-1" />
<div> <div>
@@ -131,15 +123,6 @@ pub fn StudentEligibilityPage() -> impl IntoView {
} }
}) })
.collect::<Vec<ExpandableInfo>>(); .collect::<Vec<ExpandableInfo>>();
// 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).
// We want to cast to a list, and then map the list to a new
// one that contains the correct ComparisonData.
view! { view! {
<div>{format!("Valid scholarships: {}", valid_list.len())}</div> <div>{format!("Valid scholarships: {}", valid_list.len())}</div>
<For <For
@@ -147,39 +130,23 @@ pub fn StudentEligibilityPage() -> impl IntoView {
key=|scholarship| { scholarship.subject.clone() } key=|scholarship| { scholarship.subject.clone() }
children=move |scholarship| { children=move |scholarship| {
let scholarship = StoredValue::new(scholarship); let scholarship = StoredValue::new(scholarship);
let scholarship_name = Memo::new(move |_| { let display_schema = SchemaNode::new(SchemaType::Map)
scholarship .header("Eligible Scholarship")
.read_value() .child(
.data "name",
.get("name") SchemaNode::new(SchemaType::String)
.unwrap_or(&ValueType::String(None)) .header("Scholarship Name"),
.as_string() )
.ok() .child(
.flatten() "essay_prompt",
.unwrap_or_default() SchemaNode::new(SchemaType::String).header("Essay Prompt"),
}); );
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! { view! {
<div class="flex flex-1 flex-row gap-2 p-3"> <ValueDisplay
<div>{scholarship_name}</div> schema=display_schema
<Show when=move || essay_required.get()> data_map=scholarship.get_value().data
<div>{scholarship_essay_prompt}</div> />
</Show>
</div>
} }
} }
/> />
+29 -11
View File
@@ -1,22 +1,38 @@
use crate::common::{SchemaNode, SchemaType};
use crate::components::{Loading, ValueDisplay}; use crate::components::{Loading, ValueDisplay};
use crate::pages::api::students::get_all_student_data; use crate::pages::api::students::get_all_student_data;
use crate::utils::get_user_claims; use crate::utils::get_user_claims;
use leptos::prelude::*; use leptos::prelude::*;
use crate::common::{SchemaNode, SchemaType};
#[component] #[component]
pub fn StudentTestDisplay() -> impl IntoView { pub fn StudentTestDisplay() -> impl IntoView {
let create_student_display_schema = move || { let create_student_display_schema = move || {
SchemaNode::new(SchemaType::Map) SchemaNode::new(SchemaType::Map)
.header("Student Information:") .header("Student Information")
.child("first_name", SchemaNode::new(SchemaType::String).header("First Name:")) .child(
.child("last_name", SchemaNode::new(SchemaType::String).header("Last Name:")) "first_name",
.child("sports_participation", SchemaNode::new(SchemaType::List) SchemaNode::new(SchemaType::String).header("First Name"),
.header("List of Sports Activities") )
.item_template(SchemaNode::new(SchemaType::Map) .child(
.child("sport_name", SchemaNode::new(SchemaType::String).header("Sport Name:")) "last_name",
.child("achievements", SchemaNode::new(SchemaType::String).header("Special Achievements")) SchemaNode::new(SchemaType::String).header("Last Name"),
) )
.child(
"sports_participation",
SchemaNode::new(SchemaType::List)
.header("Sports Activities")
.item_template(
SchemaNode::new(SchemaType::Map)
.header("Sport")
.child(
"sport_name",
SchemaNode::new(SchemaType::String).header("Sport Name"),
)
.child(
"achievements",
SchemaNode::new(SchemaType::String).header("Special Achievements"),
),
),
) )
}; };
@@ -31,8 +47,9 @@ pub fn StudentTestDisplay() -> impl IntoView {
let schema = StoredValue::new(create_student_display_schema()); let schema = StoredValue::new(create_student_display_schema());
view! { view! {
<div class="flex flex-1" />
<Suspense fallback=Loading> <Suspense fallback=Loading>
<div class="flex flex-1 flex-col"> <div class="flex flex-2 flex-col">
{move || { {move || {
resource resource
.get() .get()
@@ -55,5 +72,6 @@ pub fn StudentTestDisplay() -> impl IntoView {
}} }}
</div> </div>
</Suspense> </Suspense>
<div class="flex flex-1" />
} }
} }