diff --git a/src/app.rs b/src/app.rs index bfd5c97..7a27e15 100644 --- a/src/app.rs +++ b/src/app.rs @@ -158,7 +158,6 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone { path=path!("additional/specifics") view=student::StudentSpecificRequirementsPage /> - } .into_inner() diff --git a/src/common/schema_node.rs b/src/common/schema_node.rs index 2e1f943..922b3de 100644 --- a/src/common/schema_node.rs +++ b/src/common/schema_node.rs @@ -1,13 +1,35 @@ -use std::collections::HashMap; -use indexmap::IndexMap; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Default, Debug, Clone)] pub enum SchemaType { + /// Indicates a `ValueType::String` value. + #[default] String, + /// Indicates a `ValueType::Number` value. Number, - List, - Map + /// Indicates a `ValueType::List` containing only strings or numbers. + PrimitiveList, + /// Indicates a `ValueType::List` containing only maps. + MapList, + /// Indicates a `ValueType::Map`. + Map, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub enum SchemaContainerStyle { + #[default] + Header, + Capsule, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub enum SchemaHeaderStyle { + #[default] + None, + MainHeader, + SubsectionHeader, + Bold, } /// # Schema Node @@ -55,12 +77,16 @@ pub enum SchemaType { /// This schema does not currently support display of a single nested member, like a single map /// within a list. This means that you will have to display any listed or mapped information as a /// list. Consider this when designing the data. -#[derive(Deserialize, Serialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Default, Debug, Clone)] pub struct SchemaNode { + /// The display style of the data. Refer to [`SchemaContainerStyle`] for more information. + pub container_style: SchemaContainerStyle, /// The datatype of the data found using the `data_member` key. pub data_type: SchemaType, /// The header of this display. pub header: String, + /// The style of the header's display. + pub header_style: SchemaHeaderStyle, /// When used with a SchemaType::List, this determines the datatype and children for every entry /// in the list. Some examples include a list of maps, where we use the `item_template` to define /// the SchemaType::Map, and all its children. This is then mapped across all entries in the list, @@ -69,16 +95,28 @@ pub struct SchemaNode { /// When used with a SchemaType::Map, this determines what children from the map are displayed /// and how. It is ignored with all other SchemaType variants. pub children: IndexMap, + /// The primary key for iteration over maps. Used when there is a list of maps. + pub primary_key: Option, } impl SchemaNode { - pub fn new(data_type: SchemaType) -> Self { - SchemaNode { - data_type, - header: "".to_string(), - item_template: None, - children: IndexMap::new(), - } + /// Creates a new [`SchemaBuilder`]. + pub fn builder() -> SchemaBuilder { + SchemaBuilder::default() + } +} + +#[derive(Default, Debug, Clone)] +pub struct SchemaBuilder { + container_style: SchemaContainerStyle, + header: String, + header_style: SchemaHeaderStyle, +} + +impl SchemaBuilder { + pub fn container(mut self, style: SchemaContainerStyle) -> Self { + self.container_style = style; + self } pub fn header(mut self, header: impl Into) -> Self { @@ -86,13 +124,130 @@ impl SchemaNode { self } - pub fn child(mut self, data_member: impl Into, node: SchemaNode) -> Self { - self.children.insert(data_member.into(), node); + pub fn header_style(mut self, header_style: SchemaHeaderStyle) -> Self { + self.header_style = header_style; self } + pub fn primitive_list(self) -> SchemaPrimitiveListBuilder { + SchemaPrimitiveListBuilder::new(self) + } + + pub fn map_list(self) -> SchemaMapListBuilder { + SchemaMapListBuilder::new(self) + } + + pub fn map(self) -> SchemaMapBuilder { + SchemaMapBuilder::new(self) + } + + pub fn string(self) -> SchemaPrimitiveBuilder { + SchemaPrimitiveBuilder::new(self, SchemaType::String) + } + + pub fn number(self) -> SchemaPrimitiveBuilder { + SchemaPrimitiveBuilder::new(self, SchemaType::Number) + } + + fn into_node(self) -> SchemaNode { + SchemaNode { + container_style: self.container_style, + header: self.header, + header_style: self.header_style, + ..Default::default() + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct SchemaPrimitiveListBuilder { + builder: SchemaBuilder, +} + +impl SchemaPrimitiveListBuilder { + fn new(builder: SchemaBuilder) -> Self { + Self { builder } + } +} + +#[derive(Default, Debug, Clone)] +pub struct SchemaMapListBuilder { + builder: SchemaBuilder, + item_template: Option>, + primary_key: String, +} + +impl SchemaMapListBuilder { + fn new(builder: SchemaBuilder) -> Self { + Self { + builder, + ..Default::default() + } + } + pub fn item_template(mut self, node: SchemaNode) -> Self { self.item_template = Some(Box::new(node)); self } + + pub fn primary_key(mut self, primary_key: impl Into) -> Self { + self.primary_key = primary_key.into(); + self + } + + pub fn build(self) -> SchemaNode { + SchemaNode { + data_type: SchemaType::MapList, + item_template: self.item_template, + primary_key: Some(self.primary_key), + ..self.builder.into_node() + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct SchemaMapBuilder { + builder: SchemaBuilder, + children: IndexMap, +} + +impl SchemaMapBuilder { + fn new(builder: SchemaBuilder) -> Self { + Self { + builder, + ..Default::default() + } + } + + pub fn child(mut self, data_member: impl Into, node: SchemaNode) -> Self { + self.children.insert(data_member.into(), node); + self + } + + pub fn build(self) -> SchemaNode { + SchemaNode { + data_type: SchemaType::Map, + children: self.children, + ..self.builder.into_node() + } + } +} + +#[derive(Default, Debug, Clone)] +pub struct SchemaPrimitiveBuilder { + builder: SchemaBuilder, + data_type: SchemaType, +} + +impl SchemaPrimitiveBuilder { + fn new(builder: SchemaBuilder, data_type: SchemaType) -> Self { + Self { data_type, builder } + } + + pub fn build(self) -> SchemaNode { + SchemaNode { + data_type: self.data_type, + ..self.builder.into_node() + } + } } diff --git a/src/components/data_display.rs b/src/components/data_display.rs index e9b7d23..26ef721 100644 --- a/src/components/data_display.rs +++ b/src/components/data_display.rs @@ -1,265 +1,253 @@ -use crate::common::{SchemaNode, SchemaType, ValueType}; -use indexmap::IndexMap; -use leptos::logging::debug_log; +use crate::common::{SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType}; use leptos::prelude::*; use std::collections::HashMap; -/// # Value Display Component -/// This component displays any data from a given `HashMap`. It requires a schema -/// to tell it how to display the information, which is covered in the [`SchemaNode`] documentation. #[component] -pub fn ValueDisplay( - #[prop(into)] schema: Signal, - /// The starting member. Starts display from this member in the `data_map`, if it exists. +pub fn DataDisplay( + /// The schema that this component should use to display the given data. + #[prop(into)] + schema: Signal, + /// The starting key in the data map. #[prop(into, optional)] data_member: Signal, - #[prop(into)] data_map: Signal>, + /// A `HashMap` containing all the data that should be displayed. + #[prop(into)] + data_map: Signal>, ) -> impl IntoView { - let data_value = Memo::new(move |_| { - if data_member.get().is_empty() { - ValueType::Map(Some(data_map.get())) + // The schema should contain information about the header type and styling, along with the + // information display. This is rendered as part of the DataDisplayWrapper, which is simply a + // wrapper around the information to be displayed. + + // The children are all SchemaNodes as well, meaning that this component needs to call itself + // for each of these children. They will of course contain all the same information that we + // expect here. + + // The actual displayed information is controlled through a DataDisplayView, which will accept + // settings for how the information is parsed. For example, rendering a list may be done by + // a string join operation, or it may be a list of maps, each of which needs to be displayed with + // a specific set of children. The SchemaNode must be able to handle this, and these options + // should be given to the DataDisplayView somehow. + + // As the display_value here is actually an Option, we want to be able to handle cases + // where the display is given no values. In this case, the SchemaNode needs to specify whether + // a missing value is expected or an error. + + let display_value = Memo::new(move |_| { + let member = data_member.get(); + if member.is_empty() { + Some(ValueType::Map(Some(data_map.get()))) } else { - data_map - .get() - .get(&data_member.get()) - .map(|v| v.clone()) - .unwrap_or_default() + data_map.get().get(&member).cloned() } }); view! { - {move || { - match schema.get().data_type { - SchemaType::String | SchemaType::Number => { - view! { - - } - .into_any() - } - SchemaType::List => { - view! { - - } - .into_any() - } - SchemaType::Map => { - view! { - - } - .into_any() - } - } - }} + + + } } -/// # Display Wrapper Component -/// This component is designed to wrap information for the [`ValueDisplay`] component. It allows -/// for simple uniform styling of the header and capsule, along with the ability for users to -/// collapse the information. #[component] -fn DisplayWrapper( - #[prop(into, optional)] header: Signal, - #[prop(into, optional)] nested: Signal, +fn DataDisplayWrapper( + #[prop(into)] container_style: SchemaContainerStyle, + #[prop(into)] header_style: SchemaHeaderStyle, + #[prop(into)] header_text: Signal, children: Children, ) -> impl IntoView { + // This component takes a wrapper style and displays the "chrome" of the interface. It has two + // options: Text Headers or Capsules. This will then render a DataDisplayHeader, which handles + // the styling for the text inside the header specifically. + + // The capsule style is a colored header and a rounded container. + // The text header style is an invisible container. + let expanded = RwSignal::new(true); - view! { -
-
- {header} -
-
-
-
{children()}
+ match container_style { + SchemaContainerStyle::Header => { + // Creates a simple container that only has a header and a series of children. + // The spacing between the header and children is controlled by the header style. + // This type of container is not collapsable at the moment. + view! { +
+
+ +
{children()}
+
-
-
- } -} - -/// # Primitive Value Display Component -/// This component allows for the display of a `ValueType::String` or `ValueType::Number`. -#[component] -fn PrimitiveDisplay( - #[prop(into)] header: Signal, - #[prop(into)] value: Signal, - #[prop(into, optional)] nested: Signal, -) -> impl IntoView { - let display_string = Memo::new(move |_| { - let value = value.get(); - match value { - ValueType::String(_) | ValueType::Number(_) => value.to_string(), - _ => "Value is not a string.".to_string(), + } + .into_any() + } + SchemaContainerStyle::Capsule => { + // Creates a capsule container - it is collapsable, with rounded corners and a colored + // header. + view! { +
+
+ +
+
+
+
{children()}
+
+
+
+ } + .into_any() } - }); - - view! { - -
{display_string}
-
} } -/// # List Value Display Component -/// This component allows for the display of a `ValueType::List`. It will show an error if passed -/// anything other than a list. It requires an item template that tells the list how to display -/// each value inside of it, most specifically the type of data inside of it. #[component] -fn ListDisplay( - #[prop(into)] item_template: Signal>, - #[prop(into)] value: Signal, - #[prop(into)] header: Signal, - #[prop(into, optional)] nested: Signal, +fn DataDisplayHeader( + #[prop(into)] header_style: SchemaHeaderStyle, + #[prop(into)] header_text: Signal, ) -> impl IntoView { - // Unwrap the value and check if it's the correct type. To be displayed with this component, it - // MUST be a ValueType::List. - let display_list = Memo::new(move |_| { - let ValueType::List(Some(list)) = value.get() else { - return Vec::new(); - }; + // This component will take a header style and displays the corresponding text. It has a few + // options, all of which are defined in the HeaderStyle enum. - list.into_iter() - .enumerate() - .collect::>() - }); - - let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::List(_))); - - let item_header = Memo::new(move |_| item_template.get().header); - - view! { - - { - view! { }.into_any() - } - SchemaType::List => { - view! { -
"Nested lists are not currently supported for display."
- } - .into_any() - } - SchemaType::Map => { - let map_header = format!("{} {}", item_template.get().header, idx + 1); - view! { - - } - .into_any() - } - } - } - /> -
+ match header_style { + SchemaHeaderStyle::MainHeader => { + view! {
{header_text}
}.into_any() + } + SchemaHeaderStyle::SubsectionHeader => { + view! {
{header_text}
}.into_any() + } + SchemaHeaderStyle::Bold => view! {
{header_text}
}.into_any(), + SchemaHeaderStyle::None => view! {}.into_any(), } } -/// # Map Data Display Component -/// This component allows for the display of a `ValueType::Map`. The nested information must be defined -/// by a [`SchemaNode`], which tells the display what keys in the value map to search for the -/// display information. #[component] -fn MapDisplay( - #[prop(into)] value: Signal, - #[prop(into)] header: Signal, - #[prop(into)] schema: Signal>, - #[prop(into, optional)] nested: Signal, +fn DataDisplayView( + #[prop(into)] display_data: Signal>, + #[prop(into)] schema: Signal, ) -> impl IntoView { - // Unwrap the value and check if it is the correct type. It must be a ValueType::Map. - let display_map = Memo::new(move |_| { - let ValueType::Map(Some(map)) = value.get() else { - return HashMap::new(); - }; + // This is the part of the data display that does most of the work. It will take the values + // found in the DataDisplay component and perform the logic that actually displays the + // information. This component is only displayed if there is Some(value) in the DataDisplay. - map - }); + // In the case that this is displaying a Map or List, it will recursively call the DataDisplay + // component. - Effect::new(move || { - debug_log!("Current map values: {:?}", display_map.get()); - }); + // If this is displaying a PrimitiveList, it will flatten and join all the values into a single + // string, then display that string. - let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::Map(_))); + let data_type = schema.get().data_type; - view! { - - { - view! { - - } - .into_any() + // Matches the specific data type. Displays errors if the given display data does not match. + match data_type { + SchemaType::String => { + let value = Signal::derive(move || { + display_data + .get() + .map(|v| v.as_string().ok().flatten()) + .flatten() + .unwrap_or("N/A".to_string()) + }); + view! {
{value}
}.into_any() + } + SchemaType::Number => { + let value = Signal::derive(move || { + display_data + .get() + .map(|v| v.as_number().ok().flatten()) + .flatten() + .unwrap_or("N/A".to_string()) + }); + view! {
{value}
}.into_any() + } + SchemaType::PrimitiveList => { + let value = Signal::derive(move || { + display_data + .get() + .map(|v| v.as_list().ok().flatten()) + .flatten() + .unwrap_or_default() + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(", ") + }); + view! {
{value}
}.into_any() + } + SchemaType::MapList => { + let list = Signal::derive(move || { + display_data + .get() + .map(|v| v.as_list().ok().flatten()) + .flatten() + .unwrap_or_default() + .iter() + .filter_map(|v| v.as_map().ok().flatten().clone()) + .collect::>>() + }); + let primary_key = StoredValue::new(schema.get().primary_key.unwrap_or_default()); + view! { + "List is empty."
} + > + { - let template = child_schema - .item_template - .unwrap_or(Box::from(SchemaNode::new(SchemaType::List))); - + children=move |map| { view! { - } - .into_any() } - SchemaType::Map => { + /> + + } + .into_any() + } + SchemaType::Map => { + let map = Signal::derive(move || { + display_data + .get() + .map(|v| v.as_map().ok().flatten()) + .flatten() + }); + view! { + "Map was not found." } + > + } - .into_any() } - } - } - /> - + /> + + } + .into_any() + } } } diff --git a/src/pages/provider_applicants.rs b/src/pages/provider_applicants.rs index 2be2532..4f54eda 100644 --- a/src/pages/provider_applicants.rs +++ b/src/pages/provider_applicants.rs @@ -1,5 +1,8 @@ -use crate::common::{ComparisonData, ExpandableInfo, SchemaNode, SchemaType, ValueType}; -use crate::components::{ActionButton, Banner, Loading, ValueDisplay}; +use crate::common::{ + ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, + SchemaType, ValueType, +}; +use crate::components::{ActionButton, Banner, DataDisplay, Loading}; use crate::pages::UnauthenticatedPage; use crate::pages::api::get_comparison_info; use crate::pages::api::students::{GetStudentFiles, get_all_input_files, get_student_data}; @@ -9,7 +12,8 @@ use leptos::html::Dialog; use leptos::logging::debug_log; use leptos::prelude::*; use leptos::wasm_bindgen::JsCast; -use leptos::web_sys::HtmlAnchorElement; +use leptos::web_sys; +use leptos::web_sys::{HtmlAnchorElement, ScrollBehavior}; use leptos_icons::Icon; use leptos_oidc::{AuthLoaded, AuthSignal, Authenticated}; use leptos_router::components::Outlet; @@ -319,6 +323,14 @@ fn ApplicantsStudentListView( // let (current_student_id, set_current_student_id) = query_signal::("student_id"); let current_student_id = RwSignal::new(None); let navigate_student = Callback::new(move |id: String| { + let options = web_sys::ScrollToOptions::new(); + options.set_top(0.0); + options.set_behavior(ScrollBehavior::Smooth); + + if let Some(window) = web_sys::window() { + window.scroll_to_with_scroll_to_options(&options); + } + current_student_id.set(Some(id)); }); let dialog_visible = @@ -591,93 +603,172 @@ fn StudentInformationDialog( //#region Schema Definitions let demographics_schema = StoredValue::new( - SchemaNode::new(SchemaType::Map) + SchemaNode::builder() + .container(SchemaContainerStyle::Header) + .header_style(SchemaHeaderStyle::MainHeader) .header("Demographics Information") + .map() .child( "first_name", - SchemaNode::new(SchemaType::String).header("First Name"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("First Name") + .string() + .build(), ) .child( "last_name", - SchemaNode::new(SchemaType::String).header("Last Name"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Last Name") + .string() + .build(), ) .child( "dob", - SchemaNode::new(SchemaType::String).header("Date of Birth"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Date of Birth") + .string() + .build(), ) .child( "gender", - SchemaNode::new(SchemaType::String).header("Gender"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Gender") + .string() + .build(), ) .child( "phone_number", - SchemaNode::new(SchemaType::String).header("Preferred Phone Number"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Preferred Phone Number") + .string() + .build(), ) .child( "email", - SchemaNode::new(SchemaType::String).header("Preferred Email Address"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Preferred Email Address") + .string() + .build(), ) .child( "street_address", - SchemaNode::new(SchemaType::String).header("Street Address"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Street Address") + .string() + .build(), ) - .child("town", SchemaNode::new(SchemaType::String).header("Town")), + .child( + "town", + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Town") + .string() + .build(), + ) + .build(), ); let sports_schema = StoredValue::new( - SchemaNode::new(SchemaType::List) + SchemaNode::builder() + .header_style(SchemaHeaderStyle::MainHeader) .header("Sports Activities") + .map_list() + .primary_key("uuid") .item_template( - SchemaNode::new(SchemaType::Map) + SchemaNode::builder() + .container(SchemaContainerStyle::Capsule) + .header_style(SchemaHeaderStyle::Bold) .header("Sport") + .map() .child( "sport_name", - SchemaNode::new(SchemaType::String).header("Sport Name"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Sport Name") + .string() + .build(), ) .child( "achievements", - SchemaNode::new(SchemaType::String).header("Special Achievements"), - ), - ), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Special Achievements") + .string() + .build(), + ) + .build(), + ) + .build(), ); let extracurricular_schema = StoredValue::new( - SchemaNode::new(SchemaType::Map) + SchemaNode::builder() + .header_style(SchemaHeaderStyle::MainHeader) .header("Extracurricular Activities") + .map() .child( "service_hours", - SchemaNode::new(SchemaType::String).header("Total Number of Service Hours"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Total Number of Service Hours") + .number() + .build(), ) .child( "extracurricular", - SchemaNode::new(SchemaType::List) - .header("List of Activities") + SchemaNode::builder() + .map_list() + .primary_key("uuid") .item_template( - SchemaNode::new(SchemaType::Map) + SchemaNode::builder() + .container(SchemaContainerStyle::Capsule) + .header_style(SchemaHeaderStyle::Bold) .header("Activity") + .map() .child( "activity_name", - SchemaNode::new(SchemaType::String).header("Activity Name"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Activity Name") + .string() + .build(), ) .child( "num_hours", - SchemaNode::new(SchemaType::String) - .header("Number of Hours Completed"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Approximate Number of Hours Completed") + .number() + .build(), ) .child( "num_weeks", - SchemaNode::new(SchemaType::String) - .header("Number of Weeks Participated"), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Approximate Number of Weeks Participated") + .number() + .build(), ) .child( "special_involvement", - SchemaNode::new(SchemaType::String).header("Special Involvement"), - ), // .child( - // "grades", - // SchemaNode::new(SchemaType::String).header("Grades Participated") - // ) - ), - ), + SchemaNode::builder() + .header_style(SchemaHeaderStyle::Bold) + .header("Special Involvement") + .string() + .build(), + ) + .build(), + ) + .build(), + ) + .build(), ); //#endregion @@ -758,9 +849,6 @@ fn StudentInformationDialog( >

"Student Information"

-
- "Click on any header to minimize that block of information." -
@@ -781,7 +869,7 @@ fn StudentInformationDialog( .await .map(|student_info| { view! { - @@ -796,7 +884,7 @@ fn StudentInformationDialog( .await .map(|student_info| { view! { - diff --git a/src/pages/student/mod.rs b/src/pages/student/mod.rs index e625d2d..ed26209 100644 --- a/src/pages/student/mod.rs +++ b/src/pages/student/mod.rs @@ -9,10 +9,9 @@ mod financial_page; mod form_setup; mod home_page; mod shell; -mod test_display; +mod specific_requirements_page; mod university_page; mod work_experience_page; -mod specific_requirements_page; pub use academics_page::*; pub use additional_page::*; @@ -24,7 +23,6 @@ pub use family_info_page::*; pub use financial_page::*; pub use home_page::*; pub use shell::*; -pub use test_display::*; +pub use specific_requirements_page::*; pub use university_page::*; pub use work_experience_page::*; -pub use specific_requirements_page::*; diff --git a/src/pages/student/test_display.rs b/src/pages/student/test_display.rs deleted file mode 100644 index a66d8eb..0000000 --- a/src/pages/student/test_display.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::common::{SchemaNode, SchemaType}; -use crate::components::{Loading, ValueDisplay}; -use crate::pages::api::students::get_all_student_data; -use crate::utils::get_user_claims; -use leptos::prelude::*; - -#[component] -pub fn StudentTestDisplay() -> impl IntoView { - let create_student_display_schema = move || { - SchemaNode::new(SchemaType::Map) - .header("Student Information") - .child( - "first_name", - SchemaNode::new(SchemaType::String).header("First Name"), - ) - .child( - "last_name", - 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"), - ), - ), - ) - }; - - let user_claims = get_user_claims(); - let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone())); - - let resource = Resource::new( - move || user_id.get().unwrap_or_default(), - async move |id| get_all_student_data(id).await, - ); - - let schema = StoredValue::new(create_student_display_schema()); - - view! { -
- -
- {move || { - resource - .get() - .map(|data_map| { - let Ok(map) = data_map else { - return view! {}.into_any(); - }; - let data_map = StoredValue::new(map); - - view! { -
- -
- } - .into_any() - }) - }} -
-
-
- } -}