From 9925e753d893b4cb0fa86ddecc3070291ade4d99 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Wed, 27 Aug 2025 13:16:23 -0400 Subject: [PATCH] feat: implement checkboxlist component --- .cargo/config.toml | 2 + rustfmt.toml | 1 - src/app.rs | 2 +- src/components/checkboxes.rs | 88 ++++++++++++++++++++++++++++++++---- src/components/mod.rs | 2 +- src/pages/home_page.rs | 9 +++- src/pages/provider_entry.rs | 1 - style/input.css | 34 +++++++++++++- 8 files changed, 125 insertions(+), 14 deletions(-) delete mode 100644 rustfmt.toml diff --git a/.cargo/config.toml b/.cargo/config.toml index e69de29..797a3fd 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index b9d4106..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -edition = "2021" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index b95eacb..fe56c4b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,7 +60,7 @@ pub fn App() -> impl IntoView { // // sets the document title - + <Title text="R15 Scholarship App DEV"/> // content for this welcome page <Router> diff --git a/src/components/checkboxes.rs b/src/components/checkboxes.rs index 22d820d..19f8b0f 100644 --- a/src/components/checkboxes.rs +++ b/src/components/checkboxes.rs @@ -1,19 +1,91 @@ -use leptos::prelude::*; +use leptos::{leptos_dom::logging::console_log, prelude::*}; #[component] pub fn Checkbox( - #[prop()] checked: ReadSignal<bool>, - #[prop()] on_change: Callback<bool>, - #[prop()] label: String, - #[prop()] id: String + #[prop()] checked: Signal<bool>, + #[prop(into)] on_change: Callback<(), ()>, + #[prop(into)] value: String ) -> impl IntoView { - let label_id = id.clone(); view! { - <label for=label_id> + <label for=value + class="flex items-center"> <input + // Using the "peer" class links this input with the div "peer" at the same level. + // By using the "peer-checked" selector this div can toggle states based on this + // input's state (checked/unchecked) + class=" + relative peer shrink-0 + hidden" type="checkbox" - id={id} + id={value.clone()} + on:change = move |_| on_change.run(()) + prop:checked = checked /> + // This div displays the box for the checkbox, and applies some styling to the child + // span element. + <div + class="relative inline-block h-5 w-5 m-1 rounded-sm border border-gray-300 bg-white + transition-bg duration-150 peer-checked:bg-red-700 peer-checked:border-red-700 + [&>span]:opacity-0 peer-checked:[&>span]:opacity-100" + > + // This span is the check icon. It is centered within the div and has a border on the bottom + // and right sides, which is then rotated to appear like a checkmark. + <span + class="absolute left-1/2 top-1/2 block h-3 w-1.5 -translate-x-1/2 -translate-y-1/2 + rotate-45 transform border-b-3 border-r-3 + border-white transition-opacity duration-150" + ></span> + </div> + <span>{value.clone()}</span> </label> } } + +#[component] +pub fn CheckboxList( + #[prop(default = RwSignal::new(vec![]))] selected: RwSignal<Vec<String>>, + #[prop()] items: Vec<String> +) -> impl IntoView { + view! { + // Checkbox elements go in here. Each one will have a derived signal + // that checks if the name/value of that checkbox is in the selected items + // vec. If so, display as selected. If not, clear that checkbox. + + // Somehow create a derived signal?? + + { + items.into_iter().map(|item| { + let is_checked = { + let item_name = item.clone(); + move || { + // Check if this checkbox should be selected + selected.get().contains(&item_name) + } + }; + + let checked_signal = Signal::derive(is_checked); + + view! { + <Checkbox + checked = checked_signal + on_change = { + // Clone this value to avoid moving it into the closure + let item_name = item.clone(); + move || { + selected.update(|list| { + // Check if the list contains this element. If so, remove it. If not, add it. + if let Some(index) = list.iter().position(|val| *val == item_name) { + list.remove(index); + } else { + list.push(item_name.clone()); + } + }); + } + } + // The actual selected values are tracked by this element, not by the checkboxes themselves. + value = item.clone() /> + } + }).collect::<Vec<_>>() + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index d4d4e97..f699c48 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -5,4 +5,4 @@ mod text_field; mod select; mod checkboxes; -pub use self::{action_button::*, loading::*, multi_entry::*, text_field::*, select::*}; +pub use self::{action_button::*, loading::*, multi_entry::*, text_field::*, select::*, checkboxes::*}; diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 23c867d..c28b9bf 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue, Client}; use crate::app::Unauthenticated; use crate::common::{StudentInfo, StudentInfoReactive, UserClaims}; -use crate::components::{ActionButton, Loading, OutlinedTextField, Select}; +use crate::components::{ActionButton, Loading, OutlinedTextField, Select, CheckboxList}; use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated}; @@ -154,6 +154,7 @@ pub fn HomePage() -> impl IntoView { let reactive_info = StudentInfoReactive::new(submission); let select_value = RwSignal::new(String::from("Math")); + let chk_select = RwSignal::new(vec!["Testing 2".into()]); view! { <p> @@ -184,9 +185,15 @@ pub fn HomePage() -> impl IntoView { value_list = vec!["Math".into(), "English".into(), "Science".into()] value = select_value /> + <div> + <CheckboxList + selected = chk_select + items = vec!["Testing 1".into(), "Testing 2".into()] /> + </div> <div> <ActionButton on:click=move |_| { + console_log(format!("Found value {:?}", chk_select.get()).as_str()); submit_action.dispatch(CreateSampleSubmission { student_info: reactive_info.capture(), subject: user_claims.get().unwrap().claims.subject.clone() diff --git a/src/pages/provider_entry.rs b/src/pages/provider_entry.rs index b93c93b..a7143a9 100644 --- a/src/pages/provider_entry.rs +++ b/src/pages/provider_entry.rs @@ -4,7 +4,6 @@ use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, O use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated, LogoutLink}; -use serde::{Deserialize, Serialize}; #[component] pub fn ProviderEntry() -> impl IntoView { diff --git a/style/input.css b/style/input.css index aa78287..258704c 100644 --- a/style/input.css +++ b/style/input.css @@ -1 +1,33 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; + +/* Check icon styling */ +.checkbox-icon:checked::after { + content: ''; + position: absolute; + top: 50%; + bottom: 50%; + width: 5px; + height: 10px; + border: solid #fff; + border-width: 0 3px 3px 0; + transform: rotate(45deg); +} + +/* Checkbox styling */ +.checkbox-box { + position: absolute; + top: 0; + left: 0; + height: 25px; + width: 25px; + border-radius: 2px; +} + +/* + * Checkbox selected styling. + * Selects the first checkbox-icon class after a checked input element within + * this checkbox-box. + */ +.checkbox-box input:checked ~ .checkbox-icon { + background-color: var(--color-red-700); /* Use the tailwindcss variables */ +} \ No newline at end of file