Implement interactive calendar color picker
Backend enhancements: - Add calendar_path field to CalendarEvent for color mapping - Generate consistent colors for calendars using path-based hashing - Update CalDAV parsing to associate events with their calendar paths - Add 16-color palette with hash-based assignment algorithm Frontend features: - Interactive color picker with 4x4 grid of selectable colors - Click color swatches to open dropdown with all available colors - Instant color changes for both sidebar and calendar events - Persistent color preferences using local storage - Enhanced UX with hover effects and visual feedback Styling improvements: - Larger 16px color swatches for better clickability - Professional color picker dropdown with smooth animations - Dynamic event coloring based on calendar assignment - Improved contrast with text shadows and borders - Click-outside-to-close functionality for better UX Users can now personalize their calendar organization with custom colors that persist across sessions and immediately update throughout the app. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										111
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								src/app.rs
									
									
									
									
									
								
							| @@ -23,6 +23,15 @@ pub fn App() -> Html { | ||||
|     }); | ||||
|      | ||||
|     let user_info = use_state(|| -> Option<UserInfo> { None }); | ||||
|     let color_picker_open = use_state(|| -> Option<String> { None }); // Store calendar path of open picker | ||||
|      | ||||
|     // Available colors for calendar customization | ||||
|     let available_colors = [ | ||||
|         "#3B82F6", "#10B981", "#F59E0B", "#EF4444",  | ||||
|         "#8B5CF6", "#06B6D4", "#84CC16", "#F97316", | ||||
|         "#EC4899", "#6366F1", "#14B8A6", "#F3B806", | ||||
|         "#8B5A2B", "#6B7280", "#DC2626", "#7C3AED" | ||||
|     ]; | ||||
|  | ||||
|     let on_login = { | ||||
|         let auth_token = auth_token.clone(); | ||||
| @@ -67,7 +76,20 @@ pub fn App() -> Html { | ||||
|                      | ||||
|                     if !password.is_empty() { | ||||
|                         match calendar_service.fetch_user_info(&token, &password).await { | ||||
|                             Ok(info) => { | ||||
|                             Ok(mut info) => { | ||||
|                                 // Load saved colors from local storage | ||||
|                                 if let Ok(saved_colors_json) = LocalStorage::get::<String>("calendar_colors") { | ||||
|                                     if let Ok(saved_info) = serde_json::from_str::<UserInfo>(&saved_colors_json) { | ||||
|                                         // Update colors with saved preferences | ||||
|                                         for saved_cal in &saved_info.calendars { | ||||
|                                             for cal in &mut info.calendars { | ||||
|                                                 if cal.path == saved_cal.path { | ||||
|                                                     cal.color = saved_cal.color.clone(); | ||||
|                                                 } | ||||
|                                             } | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                                 user_info.set(Some(info)); | ||||
|                             } | ||||
|                             Err(err) => { | ||||
| @@ -84,9 +106,16 @@ pub fn App() -> Html { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     let on_outside_click = { | ||||
|         let color_picker_open = color_picker_open.clone(); | ||||
|         Callback::from(move |_: MouseEvent| { | ||||
|             color_picker_open.set(None); | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     html! { | ||||
|         <BrowserRouter> | ||||
|             <div class="app"> | ||||
|             <div class="app" onclick={on_outside_click}> | ||||
|                 { | ||||
|                     if auth_token.is_some() { | ||||
|                         html! { | ||||
| @@ -119,8 +148,71 @@ pub fn App() -> Html { | ||||
|                                                         <ul> | ||||
|                                                             { | ||||
|                                                                 info.calendars.iter().map(|cal| { | ||||
|                                                                     let cal_clone = cal.clone(); | ||||
|                                                                     let color_picker_open_clone = color_picker_open.clone(); | ||||
|                                                                      | ||||
|                                                                     let on_color_click = { | ||||
|                                                                         let cal_path = cal.path.clone(); | ||||
|                                                                         let color_picker_open = color_picker_open.clone(); | ||||
|                                                                         Callback::from(move |e: MouseEvent| { | ||||
|                                                                             e.stop_propagation(); | ||||
|                                                                             color_picker_open.set(Some(cal_path.clone())); | ||||
|                                                                         }) | ||||
|                                                                     }; | ||||
|                                                                      | ||||
|                                                                     html! { | ||||
|                                                                         <li key={cal.path.clone()}> | ||||
|                                                                             <span class="calendar-color"  | ||||
|                                                                                   style={format!("background-color: {}", cal.color)} | ||||
|                                                                                   onclick={on_color_click}> | ||||
|                                                                                 { | ||||
|                                                                                     if color_picker_open_clone.as_ref() == Some(&cal.path) { | ||||
|                                                                                         html! { | ||||
|                                                                                             <div class="color-picker"> | ||||
|                                                                                                 { | ||||
|                                                                                                     available_colors.iter().map(|&color| { | ||||
|                                                                                                         let color_str = color.to_string(); | ||||
|                                                                                                         let cal_path = cal.path.clone(); | ||||
|                                                                                                         let user_info_clone = user_info.clone(); | ||||
|                                                                                                         let color_picker_open = color_picker_open.clone(); | ||||
|                                                                                                          | ||||
|                                                                                                         let on_color_select = Callback::from(move |_: MouseEvent| { | ||||
|                                                                                                             // Update the calendar color locally | ||||
|                                                                                                             if let Some(mut info) = (*user_info_clone).clone() { | ||||
|                                                                                                                 for calendar in &mut info.calendars { | ||||
|                                                                                                                     if calendar.path == cal_path { | ||||
|                                                                                                                         calendar.color = color_str.clone(); | ||||
|                                                                                                                         break; | ||||
|                                                                                                                     } | ||||
|                                                                                                                 } | ||||
|                                                                                                                 user_info_clone.set(Some(info.clone())); | ||||
|                                                                                                                  | ||||
|                                                                                                                 // Save to local storage | ||||
|                                                                                                                 if let Ok(json) = serde_json::to_string(&info) { | ||||
|                                                                                                                     let _ = LocalStorage::set("calendar_colors", json); | ||||
|                                                                                                                 } | ||||
|                                                                                                             } | ||||
|                                                                                                             color_picker_open.set(None); | ||||
|                                                                                                         }); | ||||
|                                                                                                          | ||||
|                                                                                                         let is_selected = cal.color == color; | ||||
|                                                                                                         let class_name = if is_selected { "color-option selected" } else { "color-option" }; | ||||
|                                                                                                          | ||||
|                                                                                                         html! { | ||||
|                                                                                                             <div class={class_name} | ||||
|                                                                                                                  style={format!("background-color: {}", color)} | ||||
|                                                                                                                  onclick={on_color_select}> | ||||
|                                                                                                             </div> | ||||
|                                                                                                         } | ||||
|                                                                                                     }).collect::<Html>() | ||||
|                                                                                                 } | ||||
|                                                                                             </div> | ||||
|                                                                                         } | ||||
|                                                                                     } else { | ||||
|                                                                                         html! {} | ||||
|                                                                                     } | ||||
|                                                                                 } | ||||
|                                                                             </span> | ||||
|                                                                             <span class="calendar-name">{&cal.display_name}</span> | ||||
|                                                                         </li> | ||||
|                                                                     } | ||||
| @@ -162,7 +254,7 @@ pub fn App() -> Html { | ||||
|                                             } | ||||
|                                             Route::Calendar => { | ||||
|                                                 if auth_token.is_some() { | ||||
|                                                     html! { <CalendarView /> } | ||||
|                                                     html! { <CalendarView user_info={(*user_info).clone()} /> } | ||||
|                                                 } else { | ||||
|                                                     html! { <Redirect<Route> to={Route::Login}/> } | ||||
|                                                 } | ||||
| @@ -196,7 +288,7 @@ pub fn App() -> Html { | ||||
|                                         } | ||||
|                                         Route::Calendar => { | ||||
|                                             if auth_token.is_some() { | ||||
|                                                 html! { <CalendarView /> } | ||||
|                                                 html! { <CalendarView user_info={(*user_info).clone()} /> } | ||||
|                                             } else { | ||||
|                                                 html! { <Redirect<Route> to={Route::Login}/> } | ||||
|                                             } | ||||
| @@ -212,8 +304,13 @@ pub fn App() -> Html { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Properties, PartialEq)] | ||||
| pub struct CalendarViewProps { | ||||
|     pub user_info: Option<UserInfo>, | ||||
| } | ||||
|  | ||||
| #[function_component] | ||||
| fn CalendarView() -> Html { | ||||
| fn CalendarView(props: &CalendarViewProps) -> Html { | ||||
|     let events = use_state(|| HashMap::<NaiveDate, Vec<CalendarEvent>>::new()); | ||||
|     let loading = use_state(|| true); | ||||
|     let error = use_state(|| None::<String>); | ||||
| @@ -367,12 +464,12 @@ fn CalendarView() -> Html { | ||||
|                     html! { | ||||
|                         <div class="calendar-error"> | ||||
|                             <p>{format!("Error: {}", err)}</p> | ||||
|                             <Calendar events={HashMap::new()} on_event_click={dummy_callback} refreshing_event_uid={(*refreshing_event).clone()} /> | ||||
|                             <Calendar events={HashMap::new()} on_event_click={dummy_callback} refreshing_event_uid={(*refreshing_event).clone()} user_info={props.user_info.clone()} /> | ||||
|                         </div> | ||||
|                     } | ||||
|                 } else { | ||||
|                     html! { | ||||
|                         <Calendar events={(*events).clone()} on_event_click={on_event_click} refreshing_event_uid={(*refreshing_event).clone()} /> | ||||
|                         <Calendar events={(*events).clone()} on_event_click={on_event_click} refreshing_event_uid={(*refreshing_event).clone()} user_info={props.user_info.clone()} /> | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| use yew::prelude::*; | ||||
| use chrono::{Datelike, Local, NaiveDate, Duration, Weekday}; | ||||
| use std::collections::HashMap; | ||||
| use crate::services::calendar_service::CalendarEvent; | ||||
| use crate::services::calendar_service::{CalendarEvent, UserInfo}; | ||||
| use crate::components::EventModal; | ||||
|  | ||||
| #[derive(Properties, PartialEq)] | ||||
| @@ -11,6 +11,8 @@ pub struct CalendarProps { | ||||
|     pub on_event_click: Callback<CalendarEvent>, | ||||
|     #[prop_or_default] | ||||
|     pub refreshing_event_uid: Option<String>, | ||||
|     #[prop_or_default] | ||||
|     pub user_info: Option<UserInfo>, | ||||
| } | ||||
|  | ||||
| #[function_component] | ||||
| @@ -20,6 +22,21 @@ pub fn Calendar(props: &CalendarProps) -> Html { | ||||
|     let selected_day = use_state(|| today); | ||||
|     let selected_event = use_state(|| None::<CalendarEvent>); | ||||
|      | ||||
|     // Helper function to get calendar color for an event | ||||
|     let get_event_color = |event: &CalendarEvent| -> String { | ||||
|         if let Some(user_info) = &props.user_info { | ||||
|             if let Some(calendar_path) = &event.calendar_path { | ||||
|                 // Find the calendar that matches this event's path | ||||
|                 if let Some(calendar) = user_info.calendars.iter() | ||||
|                     .find(|cal| &cal.path == calendar_path) { | ||||
|                     return calendar.color.clone(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // Default color if no match found | ||||
|         "#3B82F6".to_string() | ||||
|     }; | ||||
|      | ||||
|     let first_day_of_month = current_month.with_day(1).unwrap(); | ||||
|     let days_in_month = get_days_in_month(*current_month); | ||||
|     let first_weekday = first_day_of_month.weekday(); | ||||
| @@ -118,10 +135,12 @@ pub fn Calendar(props: &CalendarProps) -> Html { | ||||
|                                                         let title = event.get_title(); | ||||
|                                                         let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); | ||||
|                                                         let class_name = if is_refreshing { "event-box refreshing" } else { "event-box" }; | ||||
|                                                         let event_color = get_event_color(&event); | ||||
|                                                         html! {  | ||||
|                                                             <div class={class_name}  | ||||
|                                                                  title={title.clone()}  | ||||
|                                                                  onclick={event_click}> | ||||
|                                                                  onclick={event_click} | ||||
|                                                                  style={format!("background-color: {}", event_color)}> | ||||
|                                                                 { | ||||
|                                                                     if is_refreshing { | ||||
|                                                                         "🔄 Refreshing...".to_string() | ||||
|   | ||||
| @@ -36,6 +36,7 @@ pub struct UserInfo { | ||||
| pub struct CalendarInfo { | ||||
|     pub path: String, | ||||
|     pub display_name: String, | ||||
|     pub color: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| @@ -59,6 +60,7 @@ pub struct CalendarEvent { | ||||
|     pub reminders: Vec<EventReminder>, | ||||
|     pub etag: Option<String>, | ||||
|     pub href: Option<String>, | ||||
|     pub calendar_path: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone