}
}
diff --git a/src/components/lists/multi/checkbox.rs b/src/components/lists/multi/checkbox.rs
index 252402d..a5de034 100644
--- a/src/components/lists/multi/checkbox.rs
+++ b/src/components/lists/multi/checkbox.rs
@@ -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,
/// The data member that this list should edit/display from the `data_map`.
#[prop(into)] data_member: Signal,
/// 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
/>
}
diff --git a/src/components/lists/multi/mod.rs b/src/components/lists/multi/mod.rs
index c3fcbbc..717cb89 100644
--- a/src/components/lists/multi/mod.rs
+++ b/src/components/lists/multi/mod.rs
@@ -1,6 +1,8 @@
mod utils;
mod checkbox;
mod chips;
+mod select_nav_list;
pub use checkbox::*;
-pub use chips::*;
\ No newline at end of file
+pub use chips::*;
+pub use select_nav_list::*;
\ No newline at end of file
diff --git a/src/components/lists/multi/select_nav_list.rs b/src/components/lists/multi/select_nav_list.rs
new file mode 100644
index 0000000..3ce9677
--- /dev/null
+++ b/src/components/lists/multi/select_nav_list.rs
@@ -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,
+ pub id: String,
+ pub slug: String,
+}
+
+#[component]
+fn NavListItem(
+ #[prop(into)] name: Signal,
+ #[prop(into)] value: Signal,
+ #[prop(into)] text: Signal>,
+ #[prop(into)] slug: Signal,
+ #[prop(into)] selected: Signal,
+ #[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::>()
+ );
+
+ view! {
+
+ }
+}
+
+#[component]
+fn NavListCheckbox(
+ #[prop(into)] selected: Signal,
+ #[prop(into)] on_change: Callback<()>,
+ #[prop(into)] name: Signal,
+ #[prop(into)] value: Signal,
+) -> impl IntoView {
+ let id = Memo::new(move |_| utils::create_unique_id(&name.get(), &value.get()));
+
+ view! {
+
+ }
+}
+
+/// # Selectable Nav-List Component
+///
+/// This component takes a `Vec` 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>,
+ #[prop(into)] items: Signal>,
+ #[prop(into)] headers: Signal>,
+ #[prop(into)] name: Signal,
+) -> 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::>()
+ );
+ }
+ };
+
+ view! {
+
+ // 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.
+
+
+
+
{header_text}
+
+
+ // Items block - contains constructed items. These are selectable.
+
+ }
+ }
+ />
+
+ }
+}
diff --git a/src/components/lists/single/radio.rs b/src/components/lists/single/radio.rs
index e5a61b7..e28c0f0 100644
--- a/src/components/lists/single/radio.rs
+++ b/src/components/lists/single/radio.rs
@@ -151,6 +151,7 @@ pub fn RadioList(
#[prop(optional, into)] label: String,
#[prop(optional, into)] required: Signal,
#[prop(optional, into)] other_prompt: Signal,
+ #[prop(optional, into)] name: Signal
) -> 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
/>
}
diff --git a/src/components/lists/single/select.rs b/src/components/lists/single/select.rs
index b20cddc..cd08b81 100644
--- a/src/components/lists/single/select.rs
+++ b/src/components/lists/single/select.rs
@@ -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,
+ #[prop(into)] value_list: Signal>,
#[prop(into)] data_member: Signal,
#[prop()] data_map: RwSignal>,
#[prop(optional, into)] label: String,
@@ -28,28 +27,19 @@ pub fn Select(
#[prop(optional, into)] required: Signal
) -> 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(_)));
@@ -68,19 +58,12 @@ pub fn Select(
//#endregion
//#region Event Logic
-
+
let on_change = move |e: Targeted| {
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.
-
- {move || {
- value_list
- .iter()
- .map(move |value| {
- let display = value.to_owned();
- view! { }
- })
- .collect::>()
- }}
+
+ // We can reasonably assume that the options in the list will be distinct.
+ {val.clone()} }
+ />
}
diff --git a/src/components/login/admin.rs b/src/components/login/admin.rs
new file mode 100644
index 0000000..0510f31
--- /dev/null
+++ b/src/components/login/admin.rs
@@ -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()} }
+}
\ No newline at end of file
diff --git a/src/components/login/mod.rs b/src/components/login/mod.rs
index fae50eb..c9911f2 100644
--- a/src/components/login/mod.rs
+++ b/src/components/login/mod.rs
@@ -1,5 +1,7 @@
+pub(crate) mod setup;
mod provider;
pub use provider::*;
mod student;
pub use student::*;
-pub(crate) mod setup;
\ No newline at end of file
+mod admin;
+pub use admin::*;
\ No newline at end of file
diff --git a/src/components/login/student.rs b/src/components/login/student.rs
index 14becf8..d5c7c54 100644
--- a/src/components/login/student.rs
+++ b/src/components/login/student.rs
@@ -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;
///
/// }
#[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! { }
+ view! { {children()} }
}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 27d6b69..89dc30c 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -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::*
};
diff --git a/src/components/multi_entry.rs b/src/components/multi_entry.rs
index c67b706..ef91df3 100644
--- a/src/components/multi_entry.rs
+++ b/src/components/multi_entry.rs
@@ -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) -> Option {
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 {
+ 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, data: &HashMap) {
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,
+ #[prop(optional, into)] description: Signal,
/// The data member that contains the list of entries within the `data_map`.
#[prop(into)]
- data_member: String,
+ data_member: Signal,
/// 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>,
/// The type and order of the inputs for each `Entry` component within this `MultiEntry` component.
- #[prop(optional)]
- schema: Vec,
+ #[prop(optional, into)]
+ schema: Signal>,
) -> 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::::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()
+ })
});
+
+ // 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_reactive.update(|list| list.push(new_entry()));
+ 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! {
+ {label}
+
+ {description}
+
+
+
// Render the entries
-
- }
+ "You haven't added any entries yet."
}
}
- />
+ >
+
+ }
+ }
+ />
+
- "Add entry"
- "Print Info"
+ "Add entry"
}
}
@@ -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>,
+ #[prop()] data_map: RwSignal>,
/// The schema of this entry. Should contain all information about what inputs to use and what
/// data members those inputs modify.
- #[prop()]
- schema: Vec,
+ #[prop(into)] schema: Signal>,
+ /// The unique ID for this entry.
+ #[prop(into)] id: Signal,
+ /// The function to run when the delete button is clicked.
+ #[prop(into)] on_delete: Callback<()>,
) -> impl IntoView {
view! {
-
}
}
diff --git a/src/components/tabs/mod.rs b/src/components/tabs/mod.rs
new file mode 100644
index 0000000..2c727dc
--- /dev/null
+++ b/src/components/tabs/mod.rs
@@ -0,0 +1,2 @@
+mod sidebar;
+pub use sidebar::*;
\ No newline at end of file
diff --git a/src/components/tabs/sidebar.rs b/src/components/tabs/sidebar.rs
new file mode 100644
index 0000000..4a2b7f0
--- /dev/null
+++ b/src/components/tabs/sidebar.rs
@@ -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`.
+/// 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! {
+///
+/// // Remember, no Outlets!
+/// }
+/// ```
+#[component]
+pub fn TabSidebarList(
+ #[prop(into)] base_path: Signal,
+ #[prop(into)] tabs: Signal>,
+) -> impl IntoView {
+ // Create a series of tabs based on a given list of names and paths.
+
+ view! {
+
+
+
+
+
+
+
+ }
+}
+
+#[component]
+fn SidebarTab(
+ #[prop(into)] base_path: Signal,
+ #[prop(into)] text: Signal,
+ #[prop(into)] path: Signal,
+ #[prop(into)] sub_paths: Signal>,
+) -> 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! {
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/pages/student/family_info_page.rs b/src/pages/student/family_info_page.rs
new file mode 100644
index 0000000..49926af
--- /dev/null
+++ b/src/pages/student/family_info_page.rs
@@ -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! {
+
+
+
+
+ // Inputs here.
+
+
+
+ // TODO Could this be a select dropdown?
+
+ // TODO Get a better example
+
+
+
+
+ // TODO Could this be a select dropdown?
+
+ // TODO Get a better example
+
+
+
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/pages/student/form_setup.rs b/src/pages/student/form_setup.rs
new file mode 100644
index 0000000..db1f166
--- /dev/null
+++ b/src/pages/student/form_setup.rs
@@ -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>,
+ pub user_id: Memo