From 927cd7d2bb56771675bec8652b435707a7356cd9 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Fri, 5 Sep 2025 12:17:09 -0400 Subject: [PATCH 1/2] Add color picker functionality to external calendars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enable clicking external calendar color icons to open color picker dropdown - Implement backend API integration for updating external calendar colors - Add conditional hover effects to prevent interference with color picker - Use extremely high z-index (999999) to ensure dropdown appears above all elements - Match existing CalDAV calendar color picker behavior and styling - Support real-time color updates with immediate visual feedback - Maintain color consistency across sidebar and calendar events 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/src/app.rs | 59 +++++++++++++++++++++++++----- frontend/src/components/sidebar.rs | 49 ++++++++++++++++++++++++- frontend/styles.css | 4 +- 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/frontend/src/app.rs b/frontend/src/app.rs index c06d85a..2110575 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -567,19 +567,60 @@ pub fn App() -> Html { let on_color_change = { let user_info = user_info.clone(); + let external_calendars = external_calendars.clone(); let color_picker_open = color_picker_open.clone(); Callback::from(move |(calendar_path, color): (String, String)| { - if let Some(mut info) = (*user_info).clone() { - for calendar in &mut info.calendars { - if calendar.path == calendar_path { - calendar.color = color.clone(); - break; - } + if calendar_path.starts_with("external_") { + // Handle external calendar color change + if let Ok(id_str) = calendar_path.strip_prefix("external_").unwrap_or("").parse::() { + let external_calendars = external_calendars.clone(); + let color = color.clone(); + + wasm_bindgen_futures::spawn_local(async move { + // Find the external calendar to get its current details + if let Some(cal) = (*external_calendars).iter().find(|c| c.id == id_str) { + match CalendarService::update_external_calendar( + id_str, + &cal.name, + &cal.url, + &color, + cal.is_visible, + ).await { + Ok(_) => { + // Update the local state + let mut updated_calendars = (*external_calendars).clone(); + for calendar in &mut updated_calendars { + if calendar.id == id_str { + calendar.color = color.clone(); + break; + } + } + external_calendars.set(updated_calendars); + + // No need to refresh events - they will automatically pick up the new color + // from the calendar when rendered since they use the same calendar_path matching + } + Err(e) => { + web_sys::console::error_1(&format!("Failed to update external calendar color: {}", e).into()); + } + } + } + }); } - user_info.set(Some(info.clone())); + } else { + // Handle CalDAV calendar color change (existing logic) + if let Some(mut info) = (*user_info).clone() { + for calendar in &mut info.calendars { + if calendar.path == calendar_path { + calendar.color = color.clone(); + break; + } + } + user_info.set(Some(info.clone())); - if let Ok(json) = serde_json::to_string(&info) { - let _ = LocalStorage::set("calendar_colors", json); + if let Ok(json) = serde_json::to_string(&info) { + let _ = LocalStorage::set("calendar_colors", json); + } } } color_picker_open.set(None); diff --git a/frontend/src/components/sidebar.rs b/frontend/src/components/sidebar.rs index d850962..5a6deae 100644 --- a/frontend/src/components/sidebar.rs +++ b/frontend/src/components/sidebar.rs @@ -256,7 +256,11 @@ pub fn sidebar(props: &SidebarProps) -> Html { html! {
  • Html { + onclick={{ + let on_color_picker_toggle = props.on_color_picker_toggle.clone(); + let external_id = format!("external_{}", cal.id); + Callback::from(move |e: MouseEvent| { + e.stop_propagation(); + on_color_picker_toggle.emit(external_id.clone()); + }) + }} + > + { + if props.color_picker_open.as_ref() == Some(&format!("external_{}", cal.id)) { + html! { +
    + { + props.available_colors.iter().map(|color| { + let color_str = color.clone(); + let external_id = format!("external_{}", cal.id); + let on_color_change = props.on_color_change.clone(); + + let on_color_select = Callback::from(move |_: MouseEvent| { + on_color_change.emit((external_id.clone(), color_str.clone())); + }); + + let is_selected = cal.color == *color; + + html! { +
    + } + }).collect::() + } +
    + } + } else { + html! {} + } + } + {&cal.name}
    { diff --git a/frontend/styles.css b/frontend/styles.css index 6469969..1e1cc13 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -281,7 +281,7 @@ body { border-radius: 4px; padding: 1rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); - z-index: 1000; + z-index: 999999; display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; @@ -3677,7 +3677,7 @@ body { border: 1px solid var(--glass-bg); } -.external-calendar-info:hover { +.external-calendar-info:hover:not(.color-picker-active) { background: var(--glass-bg); border-color: var(--glass-bg-light); transform: translateX(2px); From 91be4436a989dfd69ff67694be9c47b9c2c78e8c Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Thu, 11 Sep 2025 17:58:53 -0400 Subject: [PATCH 2/2] Fix external calendar creation and Outlook compatibility issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix external calendar form validation by replacing node refs with controlled state inputs - Add multiple user-agent fallback approach for better external calendar compatibility - Enhance HTTP client configuration with proper redirect handling and timeouts - Add detailed error logging and Outlook-specific error detection - Improve request headers for better calendar server compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/src/handlers/ics_fetcher.rs | 76 ++++++++++++++++--- .../components/calendar_management_modal.rs | 73 ++++++++++++------ 2 files changed, 118 insertions(+), 31 deletions(-) diff --git a/backend/src/handlers/ics_fetcher.rs b/backend/src/handlers/ics_fetcher.rs index edaa0d1..1fe5429 100644 --- a/backend/src/handlers/ics_fetcher.rs +++ b/backend/src/handlers/ics_fetcher.rs @@ -78,17 +78,75 @@ pub async fn fetch_external_calendar_events( // If not fetched from cache, get from external URL if !fetched_from_cache { - let client = Client::new(); - let response = client - .get(&calendar.url) - .send() - .await - .map_err(|e| ApiError::Internal(format!("Failed to fetch calendar: {}", e)))?; - - if !response.status().is_success() { - return Err(ApiError::Internal(format!("Calendar server returned: {}", response.status()))); + // Log the URL being fetched for debugging + println!("🌍 Fetching calendar URL: {}", calendar.url); + + let user_agents = vec![ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "Mozilla/5.0 (compatible; Runway Calendar/1.0)", + "Outlook-iOS/709.2226530.prod.iphone (3.24.1)" + ]; + + let mut response = None; + let mut last_error = None; + + // Try different user agents + for (i, ua) in user_agents.iter().enumerate() { + println!("🔄 Attempt {} with User-Agent: {}", i + 1, ua); + + let client = Client::builder() + .redirect(reqwest::redirect::Policy::limited(10)) + .timeout(std::time::Duration::from_secs(30)) + .user_agent(*ua) + .build() + .map_err(|e| ApiError::Internal(format!("Failed to create HTTP client: {}", e)))?; + + let result = client + .get(&calendar.url) + .header("Accept", "text/calendar,application/calendar+xml,text/plain,*/*") + .header("Accept-Charset", "utf-8") + .header("Cache-Control", "no-cache") + .send() + .await; + + match result { + Ok(resp) => { + let status = resp.status(); + println!("📡 Response status: {}", status); + if status.is_success() { + response = Some(resp); + break; + } else if status == 400 { + // Check if this is an Outlook auth error + let error_body = resp.text().await.unwrap_or_default(); + if error_body.contains("OwaPage") || error_body.contains("Outlook") { + println!("🚫 Outlook authentication error detected, trying next approach..."); + last_error = Some(format!("Outlook auth error: {}", error_body.chars().take(100).collect::())); + continue; + } + last_error = Some(format!("Bad Request: {}", error_body.chars().take(100).collect::())); + } else { + last_error = Some(format!("HTTP {}", status)); + } + } + Err(e) => { + println!("❌ Request failed: {}", e); + last_error = Some(format!("Request error: {}", e)); + } + } } + + let response = response.ok_or_else(|| { + ApiError::Internal(format!( + "Failed to fetch calendar after trying {} different approaches. Last error: {}", + user_agents.len(), + last_error.unwrap_or("Unknown error".to_string()) + )) + })?; + // Response is guaranteed to be successful here since we checked in the loop + println!("✅ Successfully fetched calendar data"); + ics_content = response .text() .await diff --git a/frontend/src/components/calendar_management_modal.rs b/frontend/src/components/calendar_management_modal.rs index 5b8d7e9..1b731c4 100644 --- a/frontend/src/components/calendar_management_modal.rs +++ b/frontend/src/components/calendar_management_modal.rs @@ -30,8 +30,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { let is_creating = use_state(|| false); // External Calendar state - let external_name_ref = use_node_ref(); - let external_url_ref = use_node_ref(); + let external_name = use_state(|| String::new()); + let external_url = use_state(|| String::new()); let external_selected_color = use_state(|| Some("#4285f4".to_string())); let external_is_loading = use_state(|| false); let external_error_message = use_state(|| None::); @@ -43,6 +43,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { let selected_color = selected_color.clone(); let create_error_message = create_error_message.clone(); let is_creating = is_creating.clone(); + let external_name = external_name.clone(); + let external_url = external_url.clone(); let external_is_loading = external_is_loading.clone(); let external_error_message = external_error_message.clone(); let external_selected_color = external_selected_color.clone(); @@ -56,6 +58,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { selected_color.set(None); create_error_message.set(None); is_creating.set(false); + external_name.set(String::new()); + external_url.set(String::new()); external_is_loading.set(false); external_error_message.set(None); external_selected_color.set(Some("#4285f4".to_string())); @@ -146,8 +150,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { // External Calendar handlers let on_external_submit = { - let external_name_ref = external_name_ref.clone(); - let external_url_ref = external_url_ref.clone(); + let external_name = external_name.clone(); + let external_url = external_url.clone(); let external_selected_color = external_selected_color.clone(); let external_is_loading = external_is_loading.clone(); let external_error_message = external_error_message.clone(); @@ -157,24 +161,28 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { Callback::from(move |e: SubmitEvent| { e.prevent_default(); - let name = external_name_ref - .cast::() - .map(|input| input.value()) - .unwrap_or_default() - .trim() - .to_string(); - - let url = external_url_ref - .cast::() - .map(|input| input.value()) - .unwrap_or_default() - .trim() - .to_string(); - + let name = (*external_name).trim().to_string(); + let url = (*external_url).trim().to_string(); let color = (*external_selected_color).clone().unwrap_or_else(|| "#4285f4".to_string()); - if name.is_empty() || url.is_empty() { - external_error_message.set(Some("Name and URL are required".to_string())); + // Debug logging to understand the issue + web_sys::console::log_1(&format!("External calendar form submission - Name: '{}', URL: '{}'", name, url).into()); + + if name.is_empty() { + external_error_message.set(Some("Calendar name is required".to_string())); + web_sys::console::log_1(&"Validation failed: empty name".into()); + return; + } + + if url.is_empty() { + external_error_message.set(Some("Calendar URL is required".to_string())); + web_sys::console::log_1(&"Validation failed: empty URL".into()); + return; + } + + // Basic URL validation + if !url.starts_with("http://") && !url.starts_with("https://") { + external_error_message.set(Some("Please enter a valid HTTP or HTTPS URL".to_string())); return; } @@ -204,6 +212,25 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { }) }; + // External input change handlers + let on_external_name_change = { + let external_name = external_name.clone(); + Callback::from(move |e: Event| { + if let Some(input) = e.target_dyn_into::() { + external_name.set(input.value()); + } + }) + }; + + let on_external_url_change = { + let external_url = external_url.clone(); + Callback::from(move |e: Event| { + if let Some(input) = e.target_dyn_into::() { + external_url.set(input.value()); + } + }) + }; + if !props.is_open { return html! {}; } @@ -333,7 +360,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { @@ -344,7 +372,8 @@ pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html {