Refactor all components to use expandable data #10

Merged
BobdaFett merged 11 commits from feature/multi-entry-data into main 2025-09-26 16:13:29 +00:00
6 changed files with 87 additions and 57 deletions
Showing only changes of commit 39996319ba - Show all commits

No files matched your search

+16 -11
View File
@@ -1,11 +1,12 @@
use crate::components::MultiEntryData; use crate::components::{MultiEntryData, ValueType};
use macros::Reactive; use macros::Reactive;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::default::Default; use std::default::Default;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use reactive_stores::Store;
/// Represents information about a student. /// Represents information about a student.
#[derive(Serialize, Deserialize, Debug, Clone, Reactive, Default)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Store, Reactive)]
pub struct StudentInfo { pub struct StudentInfo {
/// The email address of the student. This was used in the previous version of the database, /// The email address of the student. This was used in the previous version of the database,
/// and should now be deprecated. /// and should now be deprecated.
@@ -14,32 +15,36 @@ pub struct StudentInfo {
pub Email: String, pub Email: String,
/// The student's first name. /// The student's first name.
#[serde(default)] #[serde(default)]
pub first_name: String, pub first_name: ValueType, // String
/// The student's last name. /// The student's last name.
#[serde(default)] #[serde(default)]
pub last_name: String, pub last_name: ValueType, // String
/// The student's highest math score on the SAT. /// The student's highest math score on the SAT.
#[serde(default)] #[serde(default = "default_number")]
pub math_sat: i32, pub math_sat: ValueType, // Number
/// The student's desired contact email. /// The student's desired contact email.
#[serde(default)] #[serde(default)]
pub contact_email: String, pub contact_email: ValueType, // String
/// The student's address. /// The student's address.
#[serde(default)] #[serde(default)]
pub address: String, pub address: ValueType, // String
/// The student's town of residence. /// The student's town of residence.
#[serde(default)] #[serde(default)]
pub town: String, pub town: ValueType, // String
/// The student's entered phone number. /// The student's entered phone number.
#[serde(default)] #[serde(default)]
pub phone_number: String, // May be stored as a straight number in the future. pub phone_number: ValueType, // May be stored as a straight number in the future, but String
/// The student's birthdate. /// The student's birthdate.
#[serde(default)] #[serde(default)]
pub birth_date: DateTime<Utc>, pub birth_date: DateTime<Utc>,
/// The student's gender. /// The student's gender.
#[serde(default)] #[serde(default)]
pub gender: String, pub gender: ValueType, // String
/// The student's athletic information. /// The student's athletic information.
#[serde(default)] #[serde(default)]
pub athletic_info: Vec<MultiEntryData>, pub athletic_info: Vec<MultiEntryData>,
} }
fn default_number() -> ValueType {
ValueType::Number(0)
}
+2 -2
View File
@@ -9,7 +9,7 @@ use super::{multi_entry_data::*, multi_entry_member::*};
fn render_entry_component(entry_data: MultiEntryData, member_info: MultiEntryMember, label: String) -> impl IntoView { fn render_entry_component(entry_data: MultiEntryData, member_info: MultiEntryMember, label: String) -> impl IntoView {
// Parse the multientry members into the correct components // Parse the multientry members into the correct components
let data = entry_data.data.get(&member_info.member_name); let data = entry_data.data.get(&member_info.member_name);
let reactive = RwSignal::new(data.unwrap().as_string().unwrap()); let reactive = RwSignal::new(data.unwrap().clone());
let temp = either!(member_info.input_type, let temp = either!(member_info.input_type,
InputType::Text => view! { InputType::Text => view! {
<OutlinedTextField <OutlinedTextField
@@ -60,7 +60,7 @@ pub fn Entry(
let data = entry_data.clone(); let data = entry_data.clone();
let input = input_type.clone(); let input = input_type.clone();
view! { view! {
<div>{render_entry_component(data, input, "test".to_owned())}</div> // <div>{render_entry_component(data, input, "test".to_owned())}</div>
} }
}).collect::<Vec<_>>() }).collect::<Vec<_>>()
} }
+6
View File
@@ -59,6 +59,12 @@ impl ValueType {
} }
} }
impl Default for ValueType {
fn default() -> Self {
ValueType::String(String::new())
}
}
impl Display for ValueType { impl Display for ValueType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self { let str = match self {
+33 -18
View File
@@ -1,32 +1,42 @@
use leptos::prelude::*; use leptos::prelude::*;
use std::str::FromStr; use crate::components::ValueType;
#[component] #[component]
pub fn OutlinedTextField<T>( pub fn OutlinedTextField(
#[prop(optional, into)] placeholder: T, #[prop(optional, into)] placeholder: String,
#[prop(optional)] value: RwSignal<T>, // #[prop()] value: Subfield<TStore, TInner, ValueType>,
#[prop()] value: RwSignal<ValueType>,
#[prop(optional)] disabled: RwSignal<bool>, #[prop(optional)] disabled: RwSignal<bool>,
#[prop(optional)] error: RwSignal<bool>, #[prop(optional)] error: RwSignal<bool>,
#[prop(optional, into)] name: String, #[prop(optional, into)] name: String,
#[prop(optional, into)] label: String, #[prop(optional, into)] label: String,
#[prop(optional, into)] input_type: String, #[prop(optional, into)] input_type: String,
) -> impl IntoView #[prop(optional, into)] error_text: RwSignal<String>
where ) -> impl IntoView {
T: Default + 'static + FromStr + ToString + PartialEq + Send + Sync + Clone, // Check if the type is one of the supported formats. This input will only accept a string
{ // or a number value.
// Parsing logic for non-string types which happens on each input.
let on_input = move |e| { let on_input = move |e| {
let input_value = event_target_value(&e); match value.get() {
let can_parse = T::from_str(&input_value); ValueType::String(_) => {
if let Ok(parsed_value) = can_parse { // Define string parse function.
value.set(parsed_value); value.set(ValueType::String(event_target_value(&e)));
} else { },
ValueType::Number(_) => {
// Define number parse function.
let temp = event_target_value(&e);
value.set(ValueType::Number(temp.parse::<i32>().unwrap_or(0)));
},
_ => {
// Create some custom function that reports failure.
let value_type = value.get();
println!("Input {value_type:?} is not a valid type.");
error.set(true); error.set(true);
value.set(T::default()); error_text.set(format!("InputType {value_type:?} is not a valid type."));
}
} }
}; };
view! ( view! {
<div class="flex flex-1"> <div class="flex flex-1">
<label class="flex flex-col flex-1"> <label class="flex flex-col flex-1">
<span class="block ml-1.5 mb-0 font-bold">{label}</span> <span class="block ml-1.5 mb-0 font-bold">{label}</span>
@@ -37,12 +47,17 @@ where
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33" disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
r#type={input_type} r#type={input_type}
disabled={disabled} disabled={disabled}
placeholder={placeholder.to_string()} placeholder={placeholder}
prop:name=name prop:name=name
prop:value={move || value.get().to_string()} // Sets the initial value. prop:value={move || value.get().to_string()} // Sets the initial value.
on:input=on_input on:input=on_input
/> />
<div
class="text-red-600 text-sm mr-1.5 ml-1.5"
class=(["hidden"], move || !error.get())>
{error_text}
</div>
</label> </label>
</div> </div>
) }
} }
+28 -24
View File
@@ -7,10 +7,11 @@ use serde_dynamo::{from_item, to_item};
use crate::app::Unauthenticated; use crate::app::Unauthenticated;
use crate::common::{StudentInfo, UserClaims}; use crate::common::{StudentInfo, UserClaims};
use crate::components::{ActionButton, Loading, OutlinedTextField, Panel, RadioList, Row, Select}; use crate::components::{ActionButton, Loading, OutlinedTextField, Panel, Row};
use leptos::leptos_dom::logging::console_log; use leptos::leptos_dom::logging::console_log;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated}; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
use reactive_stores::Store;
use traits::{AsReactive, ReactiveCapture}; use traits::{AsReactive, ReactiveCapture};
/// # Get Student Info /// # Get Student Info
@@ -124,6 +125,7 @@ pub fn HomePage() -> impl IntoView {
server_resource server_resource
.get() .get()
.map(|submission: StudentInfo| { .map(|submission: StudentInfo| {
let store_info = Store::new(submission.clone());
let reactive_info = submission.as_reactive(); let reactive_info = submission.as_reactive();
let elements_disabled = RwSignal::new(false); let elements_disabled = RwSignal::new(false);
let result_msg = Signal::derive(move || { let result_msg = Signal::derive(move || {
@@ -197,34 +199,36 @@ pub fn HomePage() -> impl IntoView {
value=reactive_info.address value=reactive_info.address
/> />
</Row> </Row>
<Row> // <Row>
<RadioList // <RadioList
label="Town:" // label="Town:"
selected=reactive_info.town // selected=reactive_info.town
items=vec!["Southbury", "Middlebury"] // items=vec!["Southbury", "Middlebury"]
.into_iter() // .into_iter()
.map(|s| s.into()) // .map(|s| s.into())
.collect() // .collect()
disabled=elements_disabled // disabled=elements_disabled
/> // />
</Row> // </Row>
<Row> // <Row>
<Select // <Select
label="Gender:" // label="Gender:"
value_list=vec!["Male", "Female", "Prefer not to answer"] // value_list=vec!["Male", "Female", "Prefer not to answer"]
.into_iter() // .into_iter()
.map(|s| s.into()) // .map(|s| s.into())
.collect() // .collect()
value=reactive_info.gender // value=reactive_info.gender
disabled=elements_disabled // disabled=elements_disabled
/> // />
</Row> // </Row>
<Row> <Row>
<ActionButton <ActionButton
on:click=move |_| { on:click=move |_| {
let captured = reactive_info.capture();
console_log(format!("Found values: {captured:?}").as_str());
submit_action submit_action
.dispatch(CreateSampleSubmission { .dispatch(CreateSampleSubmission {
student_info: reactive_info.capture(), student_info: captured,
}); });
} }
disabled=elements_disabled disabled=elements_disabled
+2 -2
View File
@@ -1,6 +1,6 @@
use crate::app::Unauthenticated; use crate::app::Unauthenticated;
use crate::common::UserClaims; use crate::common::UserClaims;
use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField}; use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField, ValueType};
use leptos::leptos_dom::logging::console_log; use leptos::leptos_dom::logging::console_log;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated, LogoutLink}; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated, LogoutLink};
@@ -28,7 +28,7 @@ pub fn ProviderEntry() -> impl IntoView {
placeholder = "Testing information..." placeholder = "Testing information..."
name = "testing_input" name = "testing_input"
label = "This is a testing question." label = "This is a testing question."
value = RwSignal::new("".to_owned()) value = RwSignal::new(ValueType::String("".to_owned()))
/> />
</div> </div>
<div> <div>