diff --git a/backend/src/calendar.rs b/backend/src/calendar.rs index e59b148..6e6fea7 100644 --- a/backend/src/calendar.rs +++ b/backend/src/calendar.rs @@ -61,6 +61,9 @@ pub struct CalendarEvent { /// URL/href of this event on the CalDAV server pub href: Option, + + /// Calendar path this event belongs to + pub calendar_path: Option, } /// Event status enumeration @@ -182,11 +185,11 @@ impl CalDAVClient { } let body = response.text().await.map_err(CalDAVError::RequestError)?; - self.parse_calendar_response(&body) + self.parse_calendar_response(&body, calendar_path) } /// Parse CalDAV XML response containing calendar data - fn parse_calendar_response(&self, xml_response: &str) -> Result, CalDAVError> { + fn parse_calendar_response(&self, xml_response: &str, calendar_path: &str) -> Result, CalDAVError> { let mut events = Vec::new(); // Extract calendar data from XML response @@ -198,6 +201,7 @@ impl CalDAVClient { for mut event in parsed_events { event.etag = calendar_data.etag.clone(); event.href = calendar_data.href.clone(); + event.calendar_path = Some(calendar_path.to_string()); events.push(event); } } @@ -377,6 +381,7 @@ impl CalDAVClient { reminders: self.parse_alarms(&event)?, etag: None, // Set by caller href: None, // Set by caller + calendar_path: None, // Set by caller }) } diff --git a/backend/src/handlers.rs b/backend/src/handlers.rs index 6777af3..ad0dfd3 100644 --- a/backend/src/handlers.rs +++ b/backend/src/handlers.rs @@ -145,8 +145,9 @@ pub async fn get_user_info( None } else { Some(CalendarInfo { - path, + path: path.clone(), display_name, + color: generate_calendar_color(&path), }) } }).collect(); @@ -158,6 +159,39 @@ pub async fn get_user_info( })) } +// Helper function to generate a consistent color for a calendar based on its path +fn generate_calendar_color(path: &str) -> String { + // Predefined set of attractive, accessible colors for calendars + let colors = [ + "#3B82F6", // Blue + "#10B981", // Emerald + "#F59E0B", // Amber + "#EF4444", // Red + "#8B5CF6", // Violet + "#06B6D4", // Cyan + "#84CC16", // Lime + "#F97316", // Orange + "#EC4899", // Pink + "#6366F1", // Indigo + "#14B8A6", // Teal + "#F3B806", // Yellow + "#8B5A2B", // Brown + "#6B7280", // Gray + "#DC2626", // Red-600 + "#7C3AED", // Violet-600 + ]; + + // Create a simple hash from the path to ensure consistent color assignment + let mut hash: u32 = 0; + for byte in path.bytes() { + hash = hash.wrapping_mul(31).wrapping_add(byte as u32); + } + + // Use the hash to select a color from our palette + let color_index = (hash as usize) % colors.len(); + colors[color_index].to_string() +} + // Helper function to extract a readable calendar name from path fn extract_calendar_name(path: &str) -> String { // Extract the last meaningful part of the path diff --git a/backend/src/models.rs b/backend/src/models.rs index 64b3354..6252e68 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -31,6 +31,7 @@ pub struct UserInfo { pub struct CalendarInfo { pub path: String, pub display_name: String, + pub color: String, } // Error handling diff --git a/src/app.rs b/src/app.rs index cda127a..26d412b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,6 +23,15 @@ pub fn App() -> Html { }); let user_info = use_state(|| -> Option { None }); + let color_picker_open = use_state(|| -> Option { 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::("calendar_colors") { + if let Ok(saved_info) = serde_json::from_str::(&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! { -
+
{ if auth_token.is_some() { html! { @@ -119,8 +148,71 @@ pub fn App() -> Html {
    { 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! {
  • + + { + if color_picker_open_clone.as_ref() == Some(&cal.path) { + html! { +
    + { + 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! { +
    +
    + } + }).collect::() + } +
    + } + } else { + html! {} + } + } +
    {&cal.display_name}
  • } @@ -162,7 +254,7 @@ pub fn App() -> Html { } Route::Calendar => { if auth_token.is_some() { - html! { } + html! { } } else { html! { to={Route::Login}/> } } @@ -196,7 +288,7 @@ pub fn App() -> Html { } Route::Calendar => { if auth_token.is_some() { - html! { } + html! { } } else { html! { to={Route::Login}/> } } @@ -212,8 +304,13 @@ pub fn App() -> Html { } } +#[derive(Properties, PartialEq)] +pub struct CalendarViewProps { + pub user_info: Option, +} + #[function_component] -fn CalendarView() -> Html { +fn CalendarView(props: &CalendarViewProps) -> Html { let events = use_state(|| HashMap::>::new()); let loading = use_state(|| true); let error = use_state(|| None::); @@ -367,12 +464,12 @@ fn CalendarView() -> Html { html! {

    {format!("Error: {}", err)}

    - +
    } } else { html! { - + } } } diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 2628d70..3551684 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -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, #[prop_or_default] pub refreshing_event_uid: Option, + #[prop_or_default] + pub user_info: Option, } #[function_component] @@ -20,6 +22,21 @@ pub fn Calendar(props: &CalendarProps) -> Html { let selected_day = use_state(|| today); let selected_event = use_state(|| None::); + // 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! {
    + onclick={event_click} + style={format!("background-color: {}", event_color)}> { if is_refreshing { "🔄 Refreshing...".to_string() diff --git a/src/services/calendar_service.rs b/src/services/calendar_service.rs index 20b9ee9..d8671f1 100644 --- a/src/services/calendar_service.rs +++ b/src/services/calendar_service.rs @@ -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, pub etag: Option, pub href: Option, + pub calendar_path: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/styles.css b/styles.css index da9b2fc..9f068a3 100644 --- a/styles.css +++ b/styles.css @@ -136,6 +136,7 @@ body { border-radius: 6px; transition: all 0.2s; cursor: pointer; + gap: 0.75rem; } .calendar-list li:hover { @@ -143,10 +144,72 @@ body { transform: translateX(2px); } +.calendar-color { + width: 16px; + height: 16px; + border-radius: 50%; + flex-shrink: 0; + border: 2px solid rgba(255,255,255,0.3); + transition: all 0.2s; + cursor: pointer; + position: relative; +} + +.calendar-list li:hover .calendar-color { + border-color: rgba(255,255,255,0.6); + transform: scale(1.1); +} + +.color-picker { + position: absolute; + top: 100%; + left: 0; + background: white; + border-radius: 8px; + padding: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 1000; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; + min-width: 120px; + border: 1px solid rgba(0,0,0,0.1); +} + +.color-option { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid rgba(0,0,0,0.1); + cursor: pointer; + transition: all 0.2s; +} + +.color-option:hover { + transform: scale(1.2); + border-color: rgba(0,0,0,0.3); +} + +.color-option.selected { + border-color: #333; + border-width: 3px; + transform: scale(1.1); +} + +.color-picker-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 999; +} + .calendar-name { color: white; font-size: 0.9rem; font-weight: 500; + flex: 1; } .no-calendars { @@ -459,7 +522,7 @@ body { } .event-box { - background: #2196f3; + /* Background color will be set inline via style attribute */ color: white; padding: 2px 4px; border-radius: 3px; @@ -469,16 +532,34 @@ body { text-overflow: ellipsis; white-space: nowrap; cursor: pointer; - transition: background-color 0.2s; + transition: all 0.2s ease; + border: 1px solid rgba(255,255,255,0.2); + text-shadow: 0 1px 1px rgba(0,0,0,0.3); + font-weight: 500; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); + position: relative; } .event-box:hover { - background: #1976d2; + filter: brightness(1.15); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.15); } .event-box.refreshing { - background: #ff9800; animation: pulse 1.5s ease-in-out infinite alternate; + border-color: #ff9800; +} + +.event-box.refreshing::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 152, 0, 0.3); + pointer-events: none; } @keyframes pulse {