From 6a01a75cce8c00851797cb8a3bca721b33a980be Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Wed, 3 Sep 2025 21:37:46 -0400 Subject: [PATCH] Add visibility toggles for CalDAV calendars with event filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users can now toggle visibility of CalDAV calendars using checkboxes in the sidebar, matching the behavior of external calendars. Events from hidden calendars are automatically filtered out of the calendar view. Changes: - Add is_visible field to CalendarInfo (frontend & backend) - Add visibility checkboxes to CalDAV calendar list items - Implement real-time event filtering based on calendar visibility - Add CSS styling matching external calendar checkboxes - Default new calendars to visible state 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/src/handlers/auth.rs | 1 + backend/src/models.rs | 1 + frontend/src/app.rs | 15 ++++ frontend/src/components/calendar.rs | 27 +++++-- frontend/src/components/calendar_list_item.rs | 78 +++++++++++-------- frontend/src/components/sidebar.rs | 2 + frontend/src/services/calendar_service.rs | 1 + frontend/styles.css | 23 ++++++ 8 files changed, 112 insertions(+), 36 deletions(-) diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index 5c6107e..89ab1af 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -93,6 +93,7 @@ pub async fn get_user_info( path: path.clone(), display_name: extract_calendar_name(path), color: generate_calendar_color(path), + is_visible: true, // Default to visible }) .collect(); diff --git a/backend/src/models.rs b/backend/src/models.rs index a001b4a..2b77fde 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -56,6 +56,7 @@ pub struct CalendarInfo { pub path: String, pub display_name: String, pub color: String, + pub is_visible: bool, } #[derive(Debug, Deserialize)] diff --git a/frontend/src/app.rs b/frontend/src/app.rs index b667966..923da7b 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -1067,6 +1067,21 @@ pub fn App() -> Html { on_color_picker_toggle={on_color_picker_toggle} available_colors={(*available_colors).clone()} on_calendar_context_menu={on_calendar_context_menu} + on_calendar_visibility_toggle={Callback::from({ + let user_info = user_info.clone(); + move |calendar_path: String| { + let user_info = user_info.clone(); + wasm_bindgen_futures::spawn_local(async move { + if let Some(mut info) = (*user_info).clone() { + // Toggle the visibility + if let Some(calendar) = info.calendars.iter_mut().find(|c| c.path == calendar_path) { + calendar.is_visible = !calendar.is_visible; + user_info.set(Some(info)); + } + } + }); + } + })} current_view={(*current_view).clone()} on_view_change={on_view_change} current_theme={(*current_theme).clone()} diff --git a/frontend/src/components/calendar.rs b/frontend/src/components/calendar.rs index c2d8856..8608e00 100644 --- a/frontend/src/components/calendar.rs +++ b/frontend/src/components/calendar.rs @@ -108,10 +108,11 @@ pub fn Calendar(props: &CalendarProps) -> Html { let external_events = props.external_calendar_events.clone(); // Clone before the effect let view = props.view.clone(); // Clone before the effect - use_effect_with((*current_date, view.clone(), external_events.len()), move |(date, _view, _external_len)| { + use_effect_with((*current_date, view.clone(), external_events.len(), props.user_info.clone()), move |(date, _view, _external_len, user_info)| { let auth_token: Option = LocalStorage::get("auth_token").ok(); let date = *date; // Clone the date to avoid lifetime issues let external_events = external_events.clone(); // Clone external events to avoid lifetime issues + let user_info = user_info.clone(); // Clone user_info to avoid lifetime issues if let Some(token) = auth_token { let events = events.clone(); @@ -148,8 +149,24 @@ pub fn Calendar(props: &CalendarProps) -> Html { .await { Ok(vevents) => { - // Combine regular events with external calendar events - let mut all_events = vevents; + // Filter CalDAV events based on calendar visibility + let mut filtered_events = if let Some(user_info) = user_info.as_ref() { + vevents.into_iter() + .filter(|event| { + if let Some(calendar_path) = event.calendar_path.as_ref() { + // Find the calendar info for this event + user_info.calendars.iter() + .find(|cal| &cal.path == calendar_path) + .map(|cal| cal.is_visible) + .unwrap_or(true) // Default to visible if not found + } else { + true // Show events without calendar path + } + }) + .collect() + } else { + vevents // Show all events if no user info + }; // Mark external events as external by adding a special category let marked_external_events: Vec = external_events @@ -161,9 +178,9 @@ pub fn Calendar(props: &CalendarProps) -> Html { }) .collect(); - all_events.extend(marked_external_events); + filtered_events.extend(marked_external_events); - let grouped_events = CalendarService::group_events_by_date(all_events); + let grouped_events = CalendarService::group_events_by_date(filtered_events); events.set(grouped_events); loading.set(false); } diff --git a/frontend/src/components/calendar_list_item.rs b/frontend/src/components/calendar_list_item.rs index 6119bcb..555bd31 100644 --- a/frontend/src/components/calendar_list_item.rs +++ b/frontend/src/components/calendar_list_item.rs @@ -10,6 +10,7 @@ pub struct CalendarListItemProps { pub on_color_picker_toggle: Callback, // calendar_path pub available_colors: Vec, pub on_context_menu: Callback<(MouseEvent, String)>, // (event, calendar_path) + pub on_visibility_toggle: Callback, // calendar_path } #[function_component(CalendarListItem)] @@ -32,44 +33,59 @@ pub fn calendar_list_item(props: &CalendarListItemProps) -> Html { }) }; + let on_visibility_toggle = { + let cal_path = props.calendar.path.clone(); + let on_visibility_toggle = props.on_visibility_toggle.clone(); + Callback::from(move |_| { + on_visibility_toggle.emit(cal_path.clone()); + }) + }; + html! {
  • - - { - if props.color_picker_open { - html! { -
    - { - props.available_colors.iter().map(|color| { - let color_str = color.clone(); - let cal_path = props.calendar.path.clone(); - let on_color_change = props.on_color_change.clone(); +
    + + + { + if props.color_picker_open { + html! { +
    + { + props.available_colors.iter().map(|color| { + let color_str = color.clone(); + let cal_path = props.calendar.path.clone(); + let on_color_change = props.on_color_change.clone(); - let on_color_select = Callback::from(move |_: MouseEvent| { - on_color_change.emit((cal_path.clone(), color_str.clone())); - }); + let on_color_select = Callback::from(move |_: MouseEvent| { + on_color_change.emit((cal_path.clone(), color_str.clone())); + }); - let is_selected = props.calendar.color == *color; - let class_name = if is_selected { "color-option selected" } else { "color-option" }; + let is_selected = props.calendar.color == *color; + let class_name = if is_selected { "color-option selected" } else { "color-option" }; - html! { -
    -
    - } - }).collect::() - } -
    + html! { +
    +
    + } + }).collect::() + } +
    + } + } else { + html! {} } - } else { - html! {} } - } - - {&props.calendar.display_name} + + {&props.calendar.display_name} +
  • } } diff --git a/frontend/src/components/sidebar.rs b/frontend/src/components/sidebar.rs index a7d265d..3708e2c 100644 --- a/frontend/src/components/sidebar.rs +++ b/frontend/src/components/sidebar.rs @@ -110,6 +110,7 @@ pub struct SidebarProps { pub on_color_picker_toggle: Callback, pub available_colors: Vec, pub on_calendar_context_menu: Callback<(MouseEvent, String)>, + pub on_calendar_visibility_toggle: Callback, pub current_view: ViewMode, pub on_view_change: Callback, pub current_theme: Theme, @@ -221,6 +222,7 @@ pub fn sidebar(props: &SidebarProps) -> Html { on_color_picker_toggle={props.on_color_picker_toggle.clone()} available_colors={props.available_colors.clone()} on_context_menu={props.on_calendar_context_menu.clone()} + on_visibility_toggle={props.on_calendar_visibility_toggle.clone()} /> } }).collect::() diff --git a/frontend/src/services/calendar_service.rs b/frontend/src/services/calendar_service.rs index e153f7e..60b87ad 100644 --- a/frontend/src/services/calendar_service.rs +++ b/frontend/src/services/calendar_service.rs @@ -44,6 +44,7 @@ pub struct CalendarInfo { pub path: String, pub display_name: String, pub color: String, + pub is_visible: bool, } // CalendarEvent, EventStatus, and EventClass are now imported from shared library diff --git a/frontend/styles.css b/frontend/styles.css index d053dcb..249bd0c 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -3822,6 +3822,29 @@ body { flex-shrink: 0; } +/* CalDAV Calendar Styles */ +.calendar-info { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + border-radius: 8px; + transition: all 0.2s ease; + cursor: pointer; +} + +.calendar-info:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateX(2px); +} + +.calendar-info input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: rgba(255, 255, 255, 0.8); + cursor: pointer; +} + /* Create External Calendar Button */ .create-external-calendar-button { background: rgba(255, 255, 255, 0.15);