Add date list component #50

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

No files matched your search

+8 -8
View File
@@ -32,10 +32,10 @@ pub fn App() -> impl IntoView {
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css"/>
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css" />
// sets the document title
<Title text="R15 Scholarship App DEV"/>
<Title text="R15 Scholarship App DEV" />
// content for this welcome page
<Router>
@@ -102,15 +102,15 @@ pub fn AppWithRoutes() -> impl IntoView {
<ToastList>
// TODO Create a 404 page
<Routes fallback=|| "Page not found.".into_view()>
<Route path=path!("") view=HomePage/>
<Route path=path!("/about") view=AboutPage/>
<Route path=path!("/test_page") view=TestPage/>
<Route path=path!("") view=HomePage />
<Route path=path!("/about") view=AboutPage />
<Route path=path!("/test_page") view=TestPage />
<Route path=path!("/comparison") view=ComparisonTestPage />
<Route path=path!("/providers") view=ProviderPortal/>
<Route path=path!("/providers") view=ProviderPortal />
<Route path=path!("/providers/profile") view=ProviderContactPage />
<ParentRoute path=path!("/providers/scholarships") view=ScholarshipInfoPage>
<Route path=path!(":id") view=ScholarshipInfoPage/>
<Route path=path!("") view=ScholarshipInfoPage/>
<Route path=path!(":id") view=ScholarshipInfoPage />
<Route path=path!("") view=ScholarshipInfoPage />
</ParentRoute>
<ParentRoute path=path!("loaners") view=LoanerShell>
<Route path=path!("") view=LoanerFallback />
+12 -26
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
@@ -65,26 +59,20 @@ fn Date(
) -> impl IntoView {
view! {
<div class="flex items-start gap-3 rounded-lg border-grey-300 p-3
w-full text-left shadow-lg">
w-full text-left shadow-lg">
// <div class="flex-shrink-0 mt-0.5">
// <img src={icon} class="h-4 w-4" alt="icon"/>
// <img src={icon} class="h-4 w-4" alt="icon"/>
// </div>
<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()
}}
}
}
+14 -13
View File
@@ -134,25 +134,26 @@ 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| {
let item = item.get();
if list.contains(&item) {
list.retain(|val| *val != item);
} else {
list.push(item);
}
});
controller
.selected_list
.update(|list| {
let item = item.get();
if list.contains(&item) {
list.retain(|val| *val != item);
} else {
list.push(item);
}
});
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>
+60 -46
View File
@@ -45,16 +45,17 @@ 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
shadow-md/50 hover:shadow-md/70
peer-checked:bg-red-700 peer-checked:border-red-700 peer-checked:text-white
peer-disabled:peer-checked:bg-gray-700
peer-disabled:peer-checked:border-gray-700
peer-disabled:cursor-default
transition-all duration-150">
cursor-pointer text-base
bg-transparent border-1 border-gray-300
shadow-md/50 hover:shadow-md/70
peer-checked:bg-red-700 peer-checked:border-red-700 peer-checked:text-white
peer-disabled:peer-checked:bg-gray-700
peer-disabled:peer-checked:border-gray-700
peer-disabled:cursor-default
transition-all duration-150">
<span>{display_text.clone()}</span>
</div>
</label>
@@ -98,47 +99,60 @@ 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())
.into_iter()
.map(|(text, value)| {
let value = RwSignal::new(value);
{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 || {
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))
})
.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()]),
));
}
controller.dirty.set(true);
};
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)))
.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()])));
view! {
<Chip
checked=checked
on_change=on_change
value=value
display_text=text
name=data_member
disabled=disabled
/>
}
controller.dirty.set(true);
};
view! {
<Chip
checked=checked
on_change=on_change
value=value
display_text=text
name=data_member
disabled=disabled
/>
}
})
.collect_view()
}
})
.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>
+14 -11
View File
@@ -93,9 +93,9 @@ pub fn Select(
<span class="ml-1.5 mb-0 font-bold">{label}</span>
<select
class="relative flex-1 border-2 m-1.5 mt-0 p-1.5 rounded-md
transition-border duration-150
border-red-700 bg-transparent
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
transition-border duration-150
border-red-700 bg-transparent
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
on:change:target=on_change
disabled=disabled
>
@@ -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>
+7 -7
View File
@@ -144,12 +144,12 @@ pub fn OutlinedTextField(
<input
node_ref=input_ref
class="border-2 m-1.5 p-1.5 mt-0 rounded-md bg-transparent relative flex-1
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}
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
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>
+74 -83
View File
@@ -57,93 +57,84 @@ 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! {
<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 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
}
};
(Some(student), Some(comparisons), Some(scholarships)) => {
Either::Right(
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
};
let comparison_outcome = match comp_opt {
Some(comp) => {
// Perform the comparison
comp.compare(&student)
}
_ => Err("".to_string())
};
match comparison_outcome {
Ok(true) => {},
Ok(false) => final_result = Ok(false),
Err(e) => {
final_result = Err(e);
break;
view! {
<Panel>
<Row>
<p>{result_msg}</p>
</Row>
<Row>
<ActionButton on:click=move |_| {
let comp_member = "requirements".to_string();
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));
}
};
let mut final_result = Ok(true);
for val in list {
let comp_opt = match val {
ValueType::String(Some(comp_id)) => {
comparisons.iter().find(|comp| comp.id == *comp_id)
}
_ => None,
};
let comparison_outcome = match comp_opt {
Some(comp) => comp.compare(&student),
_ => Err("".to_string()),
};
match comparison_outcome {
Ok(true) => {}
Ok(false) => final_result = Ok(false),
Err(e) => {
final_result = Err(e);
break;
}
}
}
match final_result {
Ok(true) => Ok(Some(scholarship)),
Ok(false) => Ok(None),
Err(e) => Err(e),
}
})
.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)) => {
let name_member = "name".to_string();
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),
}
}
}
match final_result {
Ok(true) => Ok(Some(scholarship)),
Ok(false) => Ok(None),
Err(e) => Err(e)
}
}).collect::<Vec<Result<Option<&ExpandableInfo>, String>>>();
// Build a display 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();
strings.push(scl_name);
},
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>
})
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> }),
}
}}
</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>
+68 -63
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,69 +420,85 @@ 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| {
if let Some(student) = opt_student.as_ref() {
format!("Are you sure want to check in {} {}?", student.first_name, student.last_name)
} else {
"There is no student selected.".to_string()
}
})}
{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,
)
} 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 on_click = {
let student_click = student.clone();
move |_| {
log!("Running on_click using student {:?}", student_click);
if let Some(dialog) = dialog_ref.get() {
chosen_student.set(Some(student_click.clone()));
dialog.show_modal().expect("Couldn't show modal dialog.");
let list_view = loaner_list
.iter()
.map(|student| {
let on_click = {
let student_click = student.clone();
move |_| {
log!("Running on_click using student {:?}", student_click);
if let Some(dialog) = dialog_ref.get() {
chosen_student.set(Some(student_click.clone()));
dialog.show_modal().expect("Couldn't show modal dialog.");
}
}
};
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)
.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-1">
{student.first_name.clone()}" "{student.last_name.clone()}
</div>
<div class="flex-none">{student_date.clone()}</div>
</div>
}
};
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)
.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-1">
{student.first_name.clone()}" "{student.last_name.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>
+1 -1
View File
@@ -143,7 +143,7 @@ pub fn ProviderContactPage() -> impl IntoView {
<div class="flex flex-row">
<div class="flex flex-col flex-1" />
<Panel>
<ValidatedForm
<ValidatedForm
on_submit=on_submit
title="Contact Form"
description="Enter your contact information and click submit."
+32 -31
View File
@@ -14,19 +14,19 @@ pub fn TestPage() -> impl IntoView {
description="Edit user profile"
icon="/Person_Black.png"
path="/"
/>
/>
<DashboardButton
title="Create Scholarship"
description="Navigate to Scholarship Creation Page"
icon="/Create_Black.png"
path="/"
/>
/>
<DashboardButton
title="Applicants"
description="View Scholarship Applicants"
icon="/Form_Black.png"
path="/"
/>
/>
</section>
<section class="space-y-2">
<div class="rounded-lg shadow-lg/25 overflow-hidden">
@@ -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![
DateInfo {
title: "Title".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
},
DateInfo {
title: "Scholarships Due".to_string(),
date: DateRange::Single("November 4th, 2025".to_string()),
description: "This is a test".to_string(),
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
},
DateInfo {
title: "Scholarships Due".to_string(),
date: DateRange::Single("November 4th, 2025".to_string()),
description: "This is a test".to_string(),
status: DateStatus::Closed
}
]
/>
<DateList dates=vec![
DateInfo {
title: "Title".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,
},
DateInfo {
title: "Scholarships Due".to_string(),
date: DateRange::Single("November 4th, 2025".to_string()),
description: "This is a test".to_string(),
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,
},
DateInfo {
title: "Scholarships Due".to_string(),
date: DateRange::Single("November 4th, 2025".to_string()),
description: "This is a test".to_string(),
status: DateStatus::Closed,
},
] />
</div>
</div>
</div>