Add date list component #50

Merged
BobdaFett merged 37 commits from feat/date-component into main 2026-01-16 14:02:18 +00:00
10 changed files with 164 additions and 155 deletions
Showing only changes of commit 4e8f4a3c5e - Show all commits

No files matched your search

+10 -24
View File
@@ -8,9 +8,7 @@ fn render_dates(dates: DateRange) -> impl IntoView {
format!("{} - {}", start_date, end_date),
};
view! {
<span>{move || date_text.clone()}</span>
}
view! { <span>{move || date_text.clone()}</span> }
}
fn render_status(status: DateStatus) -> impl IntoView {
@@ -33,11 +31,7 @@ fn render_status(status: DateStatus) -> impl IntoView {
background_class
);
view! {
<span class=classes>
{move || status_text}
</span>
}
view! { <span class=classes>{move || status_text}</span> }
}
/// # Date Component
@@ -72,19 +66,13 @@ fn Date(
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="font-bold text-md text-base">
{info.get().title}
</h3>
<h3 class="font-bold text-md text-base">{info.get().title}</h3>
<h3 class="text-md text-base mt-1">
{move || render_dates(info.get().date)}
</h3>
<p class="text-sm text-gray-600">
{info.get().description}
</p>
</div>
<div class="flex-shrink-0 ml-2">
{move || render_status(info.get().status)}
<p class="text-sm text-gray-600">{info.get().description}</p>
</div>
<div class="flex-shrink-0 ml-2">{move || render_status(info.get().status)}</div>
</div>
</div>
</div>
@@ -116,14 +104,12 @@ pub fn DateList(
) -> impl IntoView {
view! {
{move || {
dates.iter()
dates
.iter()
.map(|info| {
view! {
<Date
info=info.clone()
/>
}
}).collect_view()
view! { <Date info=info.clone() /> }
})
.collect_view()
}}
}
}
+7 -6
View File
@@ -134,15 +134,13 @@ pub fn CheckboxList(
let cloned = item.clone();
move || cloned.clone()
});
let checked = Signal::derive(move || {
controller.selected_list.get().contains(&item.get())
});
let on_change = move || {
// Update the data list by checking if the value is present in it. If so,
// remove it. If not, add it.
controller.selected_list.update(|list| {
controller
.selected_list
.update(|list| {
let item = item.get();
if list.contains(&item) {
list.retain(|val| *val != item);
@@ -153,6 +151,9 @@ pub fn CheckboxList(
controller.dirty.set(true);
};
// Update the data list by checking if the value is present in it. If so,
// remove it. If not, add it.
view! {
<Checkbox
checked=checked
@@ -171,7 +172,7 @@ pub fn CheckboxList(
{move || {
match controller.error.get() {
ValidationState::Invalid(msg) => msg,
_ => "There is no error - should not see this message.".to_string()
_ => "There is no error - should not see this message.".to_string(),
}
}}
</div>
+29 -15
View File
@@ -45,7 +45,8 @@ pub fn Chip(
id=id.clone()
prop:checked=checked
on:change=move |_| on_change.run(())
disabled=disabled />
disabled=disabled
/>
<div class="rounded-full p-2.5
cursor-pointer text-base
bg-transparent border-1 border-gray-300
@@ -98,34 +99,47 @@ pub fn ChipsList(
<div class="m-1.5 mt-0 mb-0">
<span class="font-bold">{label}</span>
<div class="flex flex-row flex-wrap gap-2 mb-1">
{move || displayed_text.get().into_iter().zip(values.get())
{move || {
displayed_text
.get()
.into_iter()
.zip(values.get())
.into_iter()
.map(|(text, value)| {
let value = RwSignal::new(value);
let checked = Signal::derive(move || {
controller.selected_list.get().contains(&value.get())
});
let on_change = move || {
// Update the data list by checking if the value is present in it. If so,
// remove it. If not, add it.
let value = value.get();
let list = controller.selected_list.get();
if list.contains(&value) {
allows_multiple.get()
.then(|| controller.selected_list.update(|list| list.retain(|v| v != &value)))
allows_multiple
.get()
.then(|| {
controller
.selected_list
.update(|list| list.retain(|v| v != &value))
})
.or_else(|| Some(controller.selected_list.set(Vec::new())));
} else {
allows_multiple.get()
.then(|| controller.selected_list.update(|list| list.push(value.clone())))
.or_else(|| Some(controller.selected_list.set(vec![value.clone()])));
allows_multiple
.get()
.then(|| {
controller
.selected_list
.update(|list| list.push(value.clone()))
})
.or_else(|| Some(
controller.selected_list.set(vec![value.clone()]),
));
}
controller.dirty.set(true);
};
// Update the data list by checking if the value is present in it. If so,
// remove it. If not, add it.
view! {
<Chip
checked=checked
@@ -138,7 +152,7 @@ pub fn ChipsList(
}
})
.collect_view()
}
}}
</div>
</div>
<Show when=move || controller.show_errors.get()>
@@ -146,7 +160,7 @@ pub fn ChipsList(
{move || {
match controller.error.get() {
ValidationState::Invalid(msg) => msg,
_ => "There is no error - should not see this message.".to_string()
_ => "There is no error - should not see this message.".to_string(),
}
}}
</div>
+11 -6
View File
@@ -121,19 +121,24 @@ pub fn RadioList(
.into_iter()
.map(|item| {
let item = RwSignal::new(item);
let checked_signal = Signal::derive(move || {
selected_value.get() == item.get()
});
let on_change = move || {
// on_change only runs when the radio button is checked, not when unchecked.
// Change the selected value to the new one.
selected_value.set(item.get());
error.set(validate(selected_value.get_untracked(), required.get_untracked()));
error
.set(
validate(
selected_value.get_untracked(),
required.get_untracked(),
),
);
dirty.set(true);
};
// on_change only runs when the radio button is checked, not when unchecked.
// Change the selected value to the new one.
view! {
<Radio
checked=checked_signal
@@ -152,7 +157,7 @@ pub fn RadioList(
{move || {
match error.get() {
ValidationState::Invalid(msg) => msg,
_ => "There is no error - should not see this message.".to_string()
_ => "There is no error - should not see this message.".to_string(),
}
}}
</div>
+11 -8
View File
@@ -104,15 +104,18 @@ pub fn Select(
value=""
disabled
hidden
selected={move || raw_value.get().is_empty()}
>"Select one..."</option>
selected=move || raw_value.get().is_empty()
>
"Select one..."
</option>
{move || {
value_list.iter().map(
move |value| {
value_list
.iter()
.map(move |value| {
let display = value.to_owned();
view! { <option value={display}>{display.clone()}</option> }
}
).collect::<Vec<_>>()
view! { <option value=display>{display.clone()}</option> }
})
.collect::<Vec<_>>()
}}
</select>
</label>
@@ -122,7 +125,7 @@ pub fn Select(
{move || {
match error.get() {
ValidationState::Invalid(msg) => msg,
_ => "There is no error - should not see this message.".to_string()
_ => "There is no error - should not see this message.".to_string(),
}
}}
</div>
+4 -4
View File
@@ -147,9 +147,9 @@ pub fn OutlinedTextField(
transition-all duration-150
border-red-700 bg-transparent
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
r#type={str_type}
disabled={disabled}
placeholder={placeholder}
r#type=str_type
disabled=disabled
placeholder=placeholder
prop:name=name
prop:value=raw_value
on:input=on_input
@@ -160,7 +160,7 @@ pub fn OutlinedTextField(
{move || {
match error.get() {
ValidationState::Invalid(msg) => msg,
_ => "There is no error - should not see this message.".to_string()
_ => "There is no error - should not see this message.".to_string(),
}
}}
</div>
+29 -38
View File
@@ -57,51 +57,42 @@ pub fn ComparisonTestPage() -> impl IntoView {
let student_info = student_resource.get();
let comparison_info = comparison_resource.get();
let scholarship_info = scholarship_resource.get();
match (student_info, comparison_info, scholarship_info) {
(Some(student), Some(comparisons), Some(scholarships)) => Either::Right(view! {
(Some(student), Some(comparisons), Some(scholarships)) => {
Either::Right(
view! {
<Panel>
<Row>
<p>{result_msg}</p>
</Row>
<Row>
<ActionButton on:click=move |_| {
// Iterate over all scholarships, get the comparison IDs,
// get ComparisonData from the IDs, then perform the
// comparisons and return a boolean. Alternatively, return
// a list of comparisons that complete and a list that don't.
let comp_member = "requirements".to_string();
let comparison_results = scholarships.iter().map(|scholarship| {
let comparison_results = scholarships
.iter()
.map(|scholarship| {
let list = match scholarship.data.get(&comp_member) {
Some(ValueType::List(Some(list))) => list,
_ => {
log!("Cannot find requirements, assuming none.");
return Ok(Some(scholarship)) // students will always qualify when there are no requirements
return Ok(Some(scholarship));
}
};
let mut final_result = Ok(true);
// The list should exist, and it should contain a series of strings.
for val in list {
let comp_opt = match val {
ValueType::String(Some(comp_id)) => {
// Find the comparison
comparisons.iter().find(|comp| comp.id == *comp_id)
}
_ => None
_ => None,
};
let comparison_outcome = match comp_opt {
Some(comp) => {
// Perform the comparison
comp.compare(&student)
}
_ => Err("".to_string())
Some(comp) => comp.compare(&student),
_ => Err("".to_string()),
};
match comparison_outcome {
Ok(true) => {},
Ok(true) => {}
Ok(false) => final_result = Ok(false),
Err(e) => {
final_result = Err(e);
@@ -109,41 +100,41 @@ pub fn ComparisonTestPage() -> impl IntoView {
}
}
}
match final_result {
Ok(true) => Ok(Some(scholarship)),
Ok(false) => Ok(None),
Err(e) => Err(e)
Err(e) => Err(e),
}
}).collect::<Vec<Result<Option<&ExpandableInfo>, String>>>();
// Build a display string
})
.collect::<Vec<Result<Option<&ExpandableInfo>, String>>>();
let mut strings = Vec::new();
strings.push("Scholarship results: \n".to_string());
for result in comparison_results {
match result {
Ok(Some(scholarship_info)) => {
// Get the scholarship name.
let name_member = "name".to_string();
let scl_name = scholarship_info.data.get(&name_member).unwrap()
.as_string().unwrap().unwrap();
let scl_name = scholarship_info
.data
.get(&name_member)
.unwrap()
.as_string()
.unwrap()
.unwrap();
strings.push(scl_name);
},
Ok(None) => {},
Err(msg) => strings.push(msg)
}
Ok(None) => {}
Err(msg) => strings.push(msg),
}
}
let display_text = strings.join("\n").to_string();
result_msg.set(display_text);
}>"Test Comparison"</ActionButton>
</Row>
</Panel>
}),
_ => Either::Left(view! {
<p>"Failed API requests."</p>
})
},
)
}
_ => Either::Left(view! { <p>"Failed API requests."</p> }),
}
}}
</Suspense>
+8 -5
View File
@@ -289,7 +289,7 @@ pub fn HomePage() -> impl IntoView {
data_member="contact_email"
data_map=form_data
input_type=TextFieldType::Email(
vec!["fakeemail.org".to_string(), "gmail.com".to_string()]
vec!["fakeemail.org".to_string(), "gmail.com".to_string()],
)
required=true
/>
@@ -369,8 +369,12 @@ pub fn HomePage() -> impl IntoView {
data_map=form_data
data_member="community_involvement"
schema=vec![
input!(Text, "service_name", "Activity Name:", true, "Some service activity..."),
input!(Number, "service_hours", "Total Service Hours", true, "20")
input!(
Text, "service_name", "Activity Name:", true, "Some service activity..."
),
input!(
Number, "service_hours", "Total Service Hours", true, "20"
),
]
/>
</Row>
@@ -380,8 +384,7 @@ pub fn HomePage() -> impl IntoView {
</Row>
}
})
}}
<Row>
}} <Row>
<ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton>
</Row>
</Suspense>
+44 -39
View File
@@ -213,11 +213,7 @@ pub fn LoanerShell() -> impl IntoView {
// We'll create the page here - the sidebar contains all the buttons, while the form
// container either shows forms or shows placeholder text.
view! {
<Banner
title="Chromebook Loaners"
logo="/PHS_Stacked_Acronym.png"
path="/loaners"
/>
<Banner title="Chromebook Loaners" logo="/PHS_Stacked_Acronym.png" path="/loaners" />
<div class="flex m-5">
<div class="flex flex-1 flex-row gap-3">
<div id="sidebar" class="flex flex-1 flex-col gap-2">
@@ -247,16 +243,9 @@ pub fn LoanerShell() -> impl IntoView {
#[component]
pub fn LoanerFallback() -> impl IntoView {
view! {
<div
class="flex flex-col items-center justify-center"
use:animate=pop_in_out()
>
<div class="text-3xl font-bold mb-2">
"Chromebook Loaner Form"
</div>
<div class="text-lg">
"Select an option from the left to begin."
</div>
<div class="flex flex-col items-center justify-center" use:animate=pop_in_out()>
<div class="text-3xl font-bold mb-2">"Chromebook Loaner Form"</div>
<div class="text-lg">"Select an option from the left to begin."</div>
</div>
}
}
@@ -431,34 +420,47 @@ pub fn LoanerReturnForm() -> impl IntoView {
<dialog
use:animate=pop_in_out()
class="p-3 m-auto rounded-md shadow-lg/33 backdrop:backdrop-blur-xs backdrop:transition-backdrop-filter"
node_ref=dialog_ref>
node_ref=dialog_ref
>
<h2 class="text-2xl font-bold">"Confirmation"</h2>
<h4 class="text-xl mb-3">
{move || chosen_student.with(|opt_student| {
{move || {
chosen_student
.with(|opt_student| {
if let Some(student) = opt_student.as_ref() {
format!("Are you sure want to check in {} {}?", student.first_name, student.last_name)
format!(
"Are you sure want to check in {} {}?",
student.first_name,
student.last_name,
)
} else {
"There is no student selected.".to_string()
}
})}
})
}}
</h4>
<ActionButton on:click=close_dialog disabled=dialog_disabled>"Close"</ActionButton>
<ActionButton on:click=dialog_check_in disabled=dialog_disabled>"Check In"</ActionButton>
<ActionButton on:click=close_dialog disabled=dialog_disabled>
"Close"
</ActionButton>
<ActionButton on:click=dialog_check_in disabled=dialog_disabled>
"Check In"
</ActionButton>
</dialog>
<div
class="flex flex-col gap-2 m-2"
use:animate=pop_in_out()
>
<div class="flex flex-col gap-2 m-2" use:animate=pop_in_out()>
<div class="flex flex-col justify-center gap-2 mb-3 py-3 px-2">
<h2 class="text-2xl font-bold">"Student List"</h2>
<p class="text-lg">"Please select your name from the list."</p>
</div>
<Suspense fallback=move || "Loading...".into_view()>
<Suspense fallback=move || {
"Loading...".into_view()
}>
{move || {
match return_list_resource.get() {
Some(loaner_list) => {
let list_view = loaner_list.iter().map(|student| {
let list_view = loaner_list
.iter()
.map(|student| {
let on_click = {
let student_click = student.clone();
move |_| {
@@ -469,31 +471,34 @@ pub fn LoanerReturnForm() -> impl IntoView {
}
}
};
let naive_time = chrono::NaiveDateTime::parse_from_str(&student.date, "%H:%M, %m/%d/%Y")
let naive_time = chrono::NaiveDateTime::parse_from_str(
&student.date,
"%H:%M, %m/%d/%Y",
)
.unwrap();
let time_zone = FixedOffset::west_opt(5 * 3600).unwrap();
let student_date = time_zone.from_utc_datetime(&naive_time)
let student_date = time_zone
.from_utc_datetime(&naive_time)
.format("%l:%M %p, %m/%d/%Y")
.to_string();
log!("Found current date {:?}", student_date);
view! {
<div class="flex flex-row p-3 rounded-md cursor-pointer transition-shadow shadow-sm hover:shadow-lg/33" on:click=on_click>
<div
class="flex flex-row p-3 rounded-md cursor-pointer transition-shadow shadow-sm hover:shadow-lg/33"
on:click=on_click
>
<div class="flex-1">
{student.first_name.clone()}" "{student.last_name.clone()}
</div>
<div class="flex-none">
{student_date.clone()}
</div>
<div class="flex-none">{student_date.clone()}</div>
</div>
}
}).collect_view();
})
.collect_view();
Either::Left(list_view)
},
None => Either::Right("No loaners found.".into_view())
}
None => Either::Right("No loaners found.".into_view()),
}
}}
</Suspense>
+11 -10
View File
@@ -35,34 +35,35 @@ pub fn TestPage() -> impl IntoView {
</div>
<div class="p-2">
<div class="flex flex-col p-3 gap-3">
<DateList
dates=vec![
<DateList dates=vec![
DateInfo {
title: "Title".to_string(),
date: DateRange::Range("February 2nd, 2026".to_string(), "February 10th, 2026".to_string()),
date: DateRange::Range(
"February 2nd, 2026".to_string(),
"February 10th, 2026".to_string(),
),
description: "This is a test".to_string(),
status: DateStatus::Upcoming
status: DateStatus::Upcoming,
},
DateInfo {
title: "Scholarships Due".to_string(),
date: DateRange::Single("November 4th, 2025".to_string()),
description: "This is a test".to_string(),
status: DateStatus::Open
status: DateStatus::Open,
},
DateInfo {
title: "Scholarships Due".to_string(),
date: DateRange::Single("November 4th, 2025".to_string()),
description: "This is a test".to_string(),
status: DateStatus::Deadline
status: DateStatus::Deadline,
},
DateInfo {
title: "Scholarships Due".to_string(),
date: DateRange::Single("November 4th, 2025".to_string()),
description: "This is a test".to_string(),
status: DateStatus::Closed
}
]
/>
status: DateStatus::Closed,
},
] />
</div>
</div>
</div>