Compare commits
	
		
			4 Commits
		
	
	
		
			edb216347d
			...
			d36609d8c2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d36609d8c2 | ||
|   | e23278d71e | ||
|   | edd209238f | ||
|   | 4fbef8a5dc | 
							
								
								
									
										121
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										121
									
								
								src/app.rs
									
									
									
									
									
								
							| @@ -138,7 +138,20 @@ pub fn App() -> Html { | |||||||
|         let context_menu_open = context_menu_open.clone(); |         let context_menu_open = context_menu_open.clone(); | ||||||
|         let event_context_menu_open = event_context_menu_open.clone(); |         let event_context_menu_open = event_context_menu_open.clone(); | ||||||
|         let calendar_context_menu_open = calendar_context_menu_open.clone(); |         let calendar_context_menu_open = calendar_context_menu_open.clone(); | ||||||
|         Callback::from(move |_: MouseEvent| { |         Callback::from(move |e: MouseEvent| { | ||||||
|  |             // Check if any context menu or color picker is open | ||||||
|  |             let any_menu_open = color_picker_open.is_some() ||  | ||||||
|  |                                *context_menu_open ||  | ||||||
|  |                                *event_context_menu_open ||  | ||||||
|  |                                *calendar_context_menu_open; | ||||||
|  |              | ||||||
|  |             if any_menu_open { | ||||||
|  |                 // Prevent the default action and stop event propagation | ||||||
|  |                 e.prevent_default(); | ||||||
|  |                 e.stop_propagation(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Close all open menus/pickers | ||||||
|             color_picker_open.set(None); |             color_picker_open.set(None); | ||||||
|             context_menu_open.set(false); |             context_menu_open.set(false); | ||||||
|             event_context_menu_open.set(false); |             event_context_menu_open.set(false); | ||||||
| @@ -146,6 +159,12 @@ pub fn App() -> Html { | |||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     // Compute if any context menu is open | ||||||
|  |     let any_context_menu_open = color_picker_open.is_some() ||  | ||||||
|  |                                *context_menu_open ||  | ||||||
|  |                                *event_context_menu_open ||  | ||||||
|  |                                *calendar_context_menu_open; | ||||||
|  |  | ||||||
|     let on_color_change = { |     let on_color_change = { | ||||||
|         let user_info = user_info.clone(); |         let user_info = user_info.clone(); | ||||||
|         let color_picker_open = color_picker_open.clone(); |         let color_picker_open = color_picker_open.clone(); | ||||||
| @@ -326,6 +345,102 @@ pub fn App() -> Html { | |||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     let on_event_update = { | ||||||
|  |         let auth_token = auth_token.clone(); | ||||||
|  |         Callback::from(move |(original_event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| { | ||||||
|  |             web_sys::console::log_1(&format!("Updating event: {} to new times: {} - {}",  | ||||||
|  |                 original_event.uid,  | ||||||
|  |                 new_start.format("%Y-%m-%d %H:%M"), | ||||||
|  |                 new_end.format("%Y-%m-%d %H:%M")).into()); | ||||||
|  |              | ||||||
|  |             if let Some(token) = (*auth_token).clone() { | ||||||
|  |                 let original_event = original_event.clone(); | ||||||
|  |                 wasm_bindgen_futures::spawn_local(async move { | ||||||
|  |                     let calendar_service = CalendarService::new(); | ||||||
|  |                      | ||||||
|  |                     // Get CalDAV password from storage | ||||||
|  |                     let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") { | ||||||
|  |                         if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) { | ||||||
|  |                             credentials["password"].as_str().unwrap_or("").to_string() | ||||||
|  |                         } else { | ||||||
|  |                             String::new() | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         String::new() | ||||||
|  |                     }; | ||||||
|  |                      | ||||||
|  |                     // Convert local times to UTC for backend storage | ||||||
|  |                     let start_utc = new_start.and_local_timezone(chrono::Local).unwrap().to_utc(); | ||||||
|  |                     let end_utc = new_end.and_local_timezone(chrono::Local).unwrap().to_utc(); | ||||||
|  |                      | ||||||
|  |                     // Format UTC date and time strings for backend | ||||||
|  |                     let start_date = start_utc.format("%Y-%m-%d").to_string(); | ||||||
|  |                     let start_time = start_utc.format("%H:%M").to_string(); | ||||||
|  |                     let end_date = end_utc.format("%Y-%m-%d").to_string(); | ||||||
|  |                     let end_time = end_utc.format("%H:%M").to_string(); | ||||||
|  |                      | ||||||
|  |                     // Convert existing event data to string formats for the API | ||||||
|  |                     let status_str = match original_event.status { | ||||||
|  |                         crate::services::calendar_service::EventStatus::Tentative => "TENTATIVE".to_string(), | ||||||
|  |                         crate::services::calendar_service::EventStatus::Confirmed => "CONFIRMED".to_string(), | ||||||
|  |                         crate::services::calendar_service::EventStatus::Cancelled => "CANCELLED".to_string(), | ||||||
|  |                     }; | ||||||
|  |                      | ||||||
|  |                     let class_str = match original_event.class { | ||||||
|  |                         crate::services::calendar_service::EventClass::Public => "PUBLIC".to_string(), | ||||||
|  |                         crate::services::calendar_service::EventClass::Private => "PRIVATE".to_string(), | ||||||
|  |                         crate::services::calendar_service::EventClass::Confidential => "CONFIDENTIAL".to_string(), | ||||||
|  |                     }; | ||||||
|  |                      | ||||||
|  |                     // Convert reminders to string format | ||||||
|  |                     let reminder_str = if !original_event.reminders.is_empty() { | ||||||
|  |                         format!("{}", original_event.reminders[0].minutes_before) | ||||||
|  |                     } else { | ||||||
|  |                         "".to_string() | ||||||
|  |                     }; | ||||||
|  |                      | ||||||
|  |                     // Handle recurrence (keep existing) | ||||||
|  |                     let recurrence_str = original_event.recurrence_rule.unwrap_or_default(); | ||||||
|  |                     let recurrence_days = vec![false; 7]; // Default - could be enhanced to parse existing recurrence | ||||||
|  |                      | ||||||
|  |                     match calendar_service.update_event( | ||||||
|  |                         &token, | ||||||
|  |                         &password, | ||||||
|  |                         original_event.uid, | ||||||
|  |                         original_event.summary.unwrap_or_default(), | ||||||
|  |                         original_event.description.unwrap_or_default(), | ||||||
|  |                         start_date, | ||||||
|  |                         start_time, | ||||||
|  |                         end_date, | ||||||
|  |                         end_time, | ||||||
|  |                         original_event.location.unwrap_or_default(), | ||||||
|  |                         original_event.all_day, | ||||||
|  |                         status_str, | ||||||
|  |                         class_str, | ||||||
|  |                         original_event.priority, | ||||||
|  |                         original_event.organizer.unwrap_or_default(), | ||||||
|  |                         original_event.attendees.join(","), | ||||||
|  |                         original_event.categories.join(","), | ||||||
|  |                         reminder_str, | ||||||
|  |                         recurrence_str, | ||||||
|  |                         recurrence_days, | ||||||
|  |                         original_event.calendar_path | ||||||
|  |                     ).await { | ||||||
|  |                         Ok(_) => { | ||||||
|  |                             web_sys::console::log_1(&"Event updated successfully".into()); | ||||||
|  |                             // Trigger a page reload to refresh events from all calendars | ||||||
|  |                             web_sys::window().unwrap().location().reload().unwrap(); | ||||||
|  |                         } | ||||||
|  |                         Err(err) => { | ||||||
|  |                             web_sys::console::error_1(&format!("Failed to update event: {}", err).into()); | ||||||
|  |                             web_sys::window().unwrap().alert_with_message(&format!("Failed to update event: {}", err)).unwrap(); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     let refresh_calendars = { |     let refresh_calendars = { | ||||||
|         let auth_token = auth_token.clone(); |         let auth_token = auth_token.clone(); | ||||||
|         let user_info = user_info.clone(); |         let user_info = user_info.clone(); | ||||||
| @@ -401,6 +516,8 @@ pub fn App() -> Html { | |||||||
|                                         on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())} |                                         on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())} | ||||||
|                                         view={(*current_view).clone()} |                                         view={(*current_view).clone()} | ||||||
|                                         on_create_event_request={Some(on_event_create.clone())} |                                         on_create_event_request={Some(on_event_create.clone())} | ||||||
|  |                                         on_event_update_request={Some(on_event_update.clone())} | ||||||
|  |                                         context_menus_open={any_context_menu_open} | ||||||
|                                     /> |                                     /> | ||||||
|                                 </main> |                                 </main> | ||||||
|                             </> |                             </> | ||||||
| @@ -414,7 +531,9 @@ pub fn App() -> Html { | |||||||
|                                     on_login={on_login.clone()} |                                     on_login={on_login.clone()} | ||||||
|                                     on_event_context_menu={Some(on_event_context_menu.clone())} |                                     on_event_context_menu={Some(on_event_context_menu.clone())} | ||||||
|                                     on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())} |                                     on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())} | ||||||
|  |                                     on_event_update_request={Some(on_event_update.clone())} | ||||||
|                                     on_create_event_request={Some(on_event_create.clone())} |                                     on_create_event_request={Some(on_event_create.clone())} | ||||||
|  |                                     context_menus_open={any_context_menu_open} | ||||||
|                                 /> |                                 /> | ||||||
|                             </div> |                             </div> | ||||||
|                         } |                         } | ||||||
|   | |||||||
| @@ -23,6 +23,10 @@ pub struct CalendarProps { | |||||||
|     pub view: ViewMode, |     pub view: ViewMode, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     pub on_create_event_request: Option<Callback<EventCreationData>>, |     pub on_create_event_request: Option<Callback<EventCreationData>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub context_menus_open: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[function_component] | #[function_component] | ||||||
| @@ -64,6 +68,20 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|     let show_create_modal = use_state(|| false); |     let show_create_modal = use_state(|| false); | ||||||
|     let create_event_data = use_state(|| None::<(chrono::NaiveDate, chrono::NaiveTime, chrono::NaiveTime)>); |     let create_event_data = use_state(|| None::<(chrono::NaiveDate, chrono::NaiveTime, chrono::NaiveTime)>); | ||||||
|      |      | ||||||
|  |     // State for time increment snapping (15 or 30 minutes) | ||||||
|  |     let time_increment = use_state(|| { | ||||||
|  |         // Try to load saved time increment from localStorage | ||||||
|  |         if let Ok(saved_increment) = LocalStorage::get::<u32>("calendar_time_increment") { | ||||||
|  |             if saved_increment == 15 || saved_increment == 30 { | ||||||
|  |                 saved_increment | ||||||
|  |             } else { | ||||||
|  |                 15 | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             15 | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |      | ||||||
|     // Handle view mode changes - adjust current_date format when switching between month/week |     // Handle view mode changes - adjust current_date format when switching between month/week | ||||||
|     { |     { | ||||||
|         let current_date = current_date.clone(); |         let current_date = current_date.clone(); | ||||||
| @@ -150,6 +168,17 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
|  |     // Handle time increment toggle | ||||||
|  |     let on_time_increment_toggle = { | ||||||
|  |         let time_increment = time_increment.clone(); | ||||||
|  |         Callback::from(move |_: MouseEvent| { | ||||||
|  |             let current = *time_increment; | ||||||
|  |             let next = if current == 15 { 30 } else { 15 }; | ||||||
|  |             time_increment.set(next); | ||||||
|  |             let _ = LocalStorage::set("calendar_time_increment", next); | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |      | ||||||
|     // Handle drag-to-create event |     // Handle drag-to-create event | ||||||
|     let on_create_event = { |     let on_create_event = { | ||||||
|         let show_create_modal = show_create_modal.clone(); |         let show_create_modal = show_create_modal.clone(); | ||||||
| @@ -162,6 +191,16 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
|  |     // Handle drag-to-move event | ||||||
|  |     let on_event_update = { | ||||||
|  |         let on_event_update_request = props.on_event_update_request.clone(); | ||||||
|  |         Callback::from(move |(event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| { | ||||||
|  |             if let Some(callback) = &on_event_update_request { | ||||||
|  |                 callback.emit((event, new_start, new_end)); | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     html! { |     html! { | ||||||
|         <div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}> |         <div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}> | ||||||
|             <CalendarHeader  |             <CalendarHeader  | ||||||
| @@ -170,6 +209,8 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|                 on_prev={on_prev} |                 on_prev={on_prev} | ||||||
|                 on_next={on_next} |                 on_next={on_next} | ||||||
|                 on_today={on_today} |                 on_today={on_today} | ||||||
|  |                 time_increment={Some(*time_increment)} | ||||||
|  |                 on_time_increment_toggle={Some(on_time_increment_toggle)} | ||||||
|             /> |             /> | ||||||
|              |              | ||||||
|             { |             { | ||||||
| @@ -209,6 +250,9 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|                             on_event_context_menu={props.on_event_context_menu.clone()} |                             on_event_context_menu={props.on_event_context_menu.clone()} | ||||||
|                             on_calendar_context_menu={props.on_calendar_context_menu.clone()} |                             on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||||
|                             on_create_event={Some(on_create_event)} |                             on_create_event={Some(on_create_event)} | ||||||
|  |                             on_event_update={Some(on_event_update)} | ||||||
|  |                             context_menus_open={props.context_menus_open} | ||||||
|  |                             time_increment={*time_increment} | ||||||
|                         /> |                         /> | ||||||
|                     }, |                     }, | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -10,6 +10,10 @@ pub struct CalendarHeaderProps { | |||||||
|     pub on_prev: Callback<MouseEvent>, |     pub on_prev: Callback<MouseEvent>, | ||||||
|     pub on_next: Callback<MouseEvent>, |     pub on_next: Callback<MouseEvent>, | ||||||
|     pub on_today: Callback<MouseEvent>, |     pub on_today: Callback<MouseEvent>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub time_increment: Option<u32>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_time_increment_toggle: Option<Callback<MouseEvent>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[function_component(CalendarHeader)] | #[function_component(CalendarHeader)] | ||||||
| @@ -18,7 +22,20 @@ pub fn calendar_header(props: &CalendarHeaderProps) -> Html { | |||||||
|  |  | ||||||
|     html! { |     html! { | ||||||
|         <div class="calendar-header"> |         <div class="calendar-header"> | ||||||
|  |             <div class="header-left"> | ||||||
|                 <button class="nav-button" onclick={props.on_prev.clone()}>{"‹"}</button> |                 <button class="nav-button" onclick={props.on_prev.clone()}>{"‹"}</button> | ||||||
|  |                 { | ||||||
|  |                     if let (Some(increment), Some(callback)) = (props.time_increment, &props.on_time_increment_toggle) { | ||||||
|  |                         html! { | ||||||
|  |                             <button class="time-increment-button" onclick={callback.clone()}> | ||||||
|  |                                 {format!("{}", increment)} | ||||||
|  |                             </button> | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         html! {} | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             </div> | ||||||
|             <h2 class="month-year">{title}</h2> |             <h2 class="month-year">{title}</h2> | ||||||
|             <div class="header-right"> |             <div class="header-right"> | ||||||
|                 <button class="today-button" onclick={props.on_today.clone()}>{"Today"}</button> |                 <button class="today-button" onclick={props.on_today.clone()}>{"Today"}</button> | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| use yew::prelude::*; | use yew::prelude::*; | ||||||
| use chrono::{Datelike, NaiveDate, Weekday}; | use chrono::{Datelike, NaiveDate, Weekday}; | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
| use web_sys::MouseEvent; | use web_sys::window; | ||||||
|  | use wasm_bindgen::{prelude::*, JsCast}; | ||||||
| use crate::services::calendar_service::{CalendarEvent, UserInfo}; | use crate::services::calendar_service::{CalendarEvent, UserInfo}; | ||||||
|  |  | ||||||
| #[derive(Properties, PartialEq)] | #[derive(Properties, PartialEq)] | ||||||
| @@ -26,11 +27,58 @@ pub struct MonthViewProps { | |||||||
|  |  | ||||||
| #[function_component(MonthView)] | #[function_component(MonthView)] | ||||||
| pub fn month_view(props: &MonthViewProps) -> Html { | pub fn month_view(props: &MonthViewProps) -> Html { | ||||||
|  |     let max_events_per_day = use_state(|| 4); // Default to 4 events max | ||||||
|     let first_day_of_month = props.current_month.with_day(1).unwrap(); |     let first_day_of_month = props.current_month.with_day(1).unwrap(); | ||||||
|     let days_in_month = get_days_in_month(props.current_month); |     let days_in_month = get_days_in_month(props.current_month); | ||||||
|     let first_weekday = first_day_of_month.weekday(); |     let first_weekday = first_day_of_month.weekday(); | ||||||
|     let days_from_prev_month = get_days_from_previous_month(props.current_month, first_weekday); |     let days_from_prev_month = get_days_from_previous_month(props.current_month, first_weekday); | ||||||
|  |  | ||||||
|  |     // Calculate maximum events that can fit based on available height | ||||||
|  |     let calculate_max_events = { | ||||||
|  |         let max_events_per_day = max_events_per_day.clone(); | ||||||
|  |         move || { | ||||||
|  |             // Since we're using CSS Grid with equal row heights, | ||||||
|  |             // we can estimate based on typical calendar dimensions | ||||||
|  |             // Typical calendar height is around 600-800px for 6 rows | ||||||
|  |             // Each row gets ~100-133px, minus day number and padding leaves ~70-100px | ||||||
|  |             // Each event is ~18px, so we can fit ~3-4 events + "+n more" indicator | ||||||
|  |             max_events_per_day.set(3); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Setup resize handler and initial calculation | ||||||
|  |     { | ||||||
|  |         let calculate_max_events = calculate_max_events.clone(); | ||||||
|  |         use_effect_with((), move |_| { | ||||||
|  |             let calculate_max_events_clone = calculate_max_events.clone(); | ||||||
|  |              | ||||||
|  |             // Initial calculation with a slight delay to ensure DOM is ready | ||||||
|  |             if let Some(window) = window() { | ||||||
|  |                 let timeout_closure = Closure::wrap(Box::new(move || { | ||||||
|  |                     calculate_max_events_clone(); | ||||||
|  |                 }) as Box<dyn FnMut()>); | ||||||
|  |                  | ||||||
|  |                 let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0( | ||||||
|  |                     timeout_closure.as_ref().unchecked_ref(), | ||||||
|  |                     100, | ||||||
|  |                 ); | ||||||
|  |                 timeout_closure.forget(); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Setup resize listener | ||||||
|  |             let resize_closure = Closure::wrap(Box::new(move || { | ||||||
|  |                 calculate_max_events(); | ||||||
|  |             }) as Box<dyn Fn()>); | ||||||
|  |              | ||||||
|  |             if let Some(window) = window() { | ||||||
|  |                 let _ = window.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref()); | ||||||
|  |                 resize_closure.forget(); // Keep the closure alive | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             || {} | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Helper function to get calendar color for an event |     // Helper function to get calendar color for an event | ||||||
|     let get_event_color = |event: &CalendarEvent| -> String { |     let get_event_color = |event: &CalendarEvent| -> String { | ||||||
|         if let Some(user_info) = &props.user_info { |         if let Some(user_info) = &props.user_info { | ||||||
| @@ -72,6 +120,11 @@ pub fn month_view(props: &MonthViewProps) -> Html { | |||||||
|                     let is_selected = props.selected_date == Some(date); |                     let is_selected = props.selected_date == Some(date); | ||||||
|                     let day_events = props.events.get(&date).cloned().unwrap_or_default(); |                     let day_events = props.events.get(&date).cloned().unwrap_or_default(); | ||||||
|                      |                      | ||||||
|  |                     // Calculate visible events and overflow | ||||||
|  |                     let max_events = *max_events_per_day as usize; | ||||||
|  |                     let visible_events: Vec<_> = day_events.iter().take(max_events).collect(); | ||||||
|  |                     let hidden_count = day_events.len().saturating_sub(max_events); | ||||||
|  |                      | ||||||
|                     html! { |                     html! { | ||||||
|                         <div  |                         <div  | ||||||
|                             class={classes!( |                             class={classes!( | ||||||
| @@ -105,14 +158,14 @@ pub fn month_view(props: &MonthViewProps) -> Html { | |||||||
|                             <div class="day-number">{day}</div> |                             <div class="day-number">{day}</div> | ||||||
|                             <div class="day-events"> |                             <div class="day-events"> | ||||||
|                                 { |                                 { | ||||||
|                                     day_events.iter().map(|event| { |                                     visible_events.iter().map(|event| { | ||||||
|                                         let event_color = get_event_color(event); |                                         let event_color = get_event_color(event); | ||||||
|                                         let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); |                                         let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); | ||||||
|                                          |                                          | ||||||
|                                         let onclick = { |                                         let onclick = { | ||||||
|                                             let on_event_click = props.on_event_click.clone(); |                                             let on_event_click = props.on_event_click.clone(); | ||||||
|                                             let event = event.clone(); |                                             let event = (*event).clone(); | ||||||
|                                             Callback::from(move |_: MouseEvent| { |                                             Callback::from(move |_: web_sys::MouseEvent| { | ||||||
|                                                 on_event_click.emit(event.clone()); |                                                 on_event_click.emit(event.clone()); | ||||||
|                                             }) |                                             }) | ||||||
|                                         }; |                                         }; | ||||||
| @@ -120,7 +173,7 @@ pub fn month_view(props: &MonthViewProps) -> Html { | |||||||
|                                         let oncontextmenu = { |                                         let oncontextmenu = { | ||||||
|                                             if let Some(callback) = &props.on_event_context_menu { |                                             if let Some(callback) = &props.on_event_context_menu { | ||||||
|                                                 let callback = callback.clone(); |                                                 let callback = callback.clone(); | ||||||
|                                                 let event = event.clone(); |                                                 let event = (*event).clone(); | ||||||
|                                                 Some(Callback::from(move |e: web_sys::MouseEvent| { |                                                 Some(Callback::from(move |e: web_sys::MouseEvent| { | ||||||
|                                                     e.prevent_default(); |                                                     e.prevent_default(); | ||||||
|                                                     callback.emit((e, event.clone())); |                                                     callback.emit((e, event.clone())); | ||||||
| @@ -142,6 +195,17 @@ pub fn month_view(props: &MonthViewProps) -> Html { | |||||||
|                                         } |                                         } | ||||||
|                                     }).collect::<Html>() |                                     }).collect::<Html>() | ||||||
|                                 } |                                 } | ||||||
|  |                                 { | ||||||
|  |                                     if hidden_count > 0 { | ||||||
|  |                                         html! { | ||||||
|  |                                             <div class="more-events-indicator"> | ||||||
|  |                                                 {format!("+{} more", hidden_count)} | ||||||
|  |                                             </div> | ||||||
|  |                                         } | ||||||
|  |                                     } else { | ||||||
|  |                                         html! {} | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|                             </div> |                             </div> | ||||||
|                         </div> |                         </div> | ||||||
|                     } |                     } | ||||||
|   | |||||||
| @@ -26,6 +26,10 @@ pub struct RouteHandlerProps { | |||||||
|     pub view: ViewMode, |     pub view: ViewMode, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>, |     pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub context_menus_open: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[function_component(RouteHandler)] | #[function_component(RouteHandler)] | ||||||
| @@ -37,6 +41,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { | |||||||
|     let on_calendar_context_menu = props.on_calendar_context_menu.clone(); |     let on_calendar_context_menu = props.on_calendar_context_menu.clone(); | ||||||
|     let view = props.view.clone(); |     let view = props.view.clone(); | ||||||
|     let on_create_event_request = props.on_create_event_request.clone(); |     let on_create_event_request = props.on_create_event_request.clone(); | ||||||
|  |     let on_event_update_request = props.on_event_update_request.clone(); | ||||||
|  |     let context_menus_open = props.context_menus_open; | ||||||
|      |      | ||||||
|     html! { |     html! { | ||||||
|         <Switch<Route> render={move |route| { |         <Switch<Route> render={move |route| { | ||||||
| @@ -47,6 +53,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { | |||||||
|             let on_calendar_context_menu = on_calendar_context_menu.clone(); |             let on_calendar_context_menu = on_calendar_context_menu.clone(); | ||||||
|             let view = view.clone(); |             let view = view.clone(); | ||||||
|             let on_create_event_request = on_create_event_request.clone(); |             let on_create_event_request = on_create_event_request.clone(); | ||||||
|  |             let on_event_update_request = on_event_update_request.clone(); | ||||||
|  |             let context_menus_open = context_menus_open; | ||||||
|              |              | ||||||
|             match route { |             match route { | ||||||
|                 Route::Home => { |                 Route::Home => { | ||||||
| @@ -72,6 +80,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { | |||||||
|                                 on_calendar_context_menu={on_calendar_context_menu} |                                 on_calendar_context_menu={on_calendar_context_menu} | ||||||
|                                 view={view} |                                 view={view} | ||||||
|                                 on_create_event_request={on_create_event_request} |                                 on_create_event_request={on_create_event_request} | ||||||
|  |                                 on_event_update_request={on_event_update_request} | ||||||
|  |                                 context_menus_open={context_menus_open} | ||||||
|                             />  |                             />  | ||||||
|                         } |                         } | ||||||
|                     } else { |                     } else { | ||||||
| @@ -94,6 +104,10 @@ pub struct CalendarViewProps { | |||||||
|     pub view: ViewMode, |     pub view: ViewMode, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>, |     pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub context_menus_open: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
| use gloo_storage::{LocalStorage, Storage}; | use gloo_storage::{LocalStorage, Storage}; | ||||||
| @@ -254,6 +268,8 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html { | |||||||
|                                 on_calendar_context_menu={props.on_calendar_context_menu.clone()} |                                 on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||||
|                                 view={props.view.clone()} |                                 view={props.view.clone()} | ||||||
|                                 on_create_event_request={props.on_create_event_request.clone()} |                                 on_create_event_request={props.on_create_event_request.clone()} | ||||||
|  |                                 on_event_update_request={props.on_event_update_request.clone()} | ||||||
|  |                                 context_menus_open={props.context_menus_open} | ||||||
|                             /> |                             /> | ||||||
|                         </div> |                         </div> | ||||||
|                     } |                     } | ||||||
| @@ -268,6 +284,8 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html { | |||||||
|                             on_calendar_context_menu={props.on_calendar_context_menu.clone()} |                             on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||||
|                             view={props.view.clone()} |                             view={props.view.clone()} | ||||||
|                             on_create_event_request={props.on_create_event_request.clone()} |                             on_create_event_request={props.on_create_event_request.clone()} | ||||||
|  |                             on_event_update_request={props.on_event_update_request.clone()} | ||||||
|  |                             context_menus_open={props.context_menus_open} | ||||||
|                         /> |                         /> | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -20,14 +20,28 @@ pub struct WeekViewProps { | |||||||
|     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>, |     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     pub on_create_event: Option<Callback<(NaiveDate, NaiveDateTime, NaiveDateTime)>>, |     pub on_create_event: Option<Callback<(NaiveDate, NaiveDateTime, NaiveDateTime)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_event_update: Option<Callback<(CalendarEvent, NaiveDateTime, NaiveDateTime)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub context_menus_open: bool, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub time_increment: u32, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Clone, PartialEq)] | ||||||
|  | enum DragType { | ||||||
|  |     CreateEvent, | ||||||
|  |     MoveEvent(CalendarEvent), | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Clone, PartialEq)] | #[derive(Clone, PartialEq)] | ||||||
| struct DragState { | struct DragState { | ||||||
|     is_dragging: bool, |     is_dragging: bool, | ||||||
|  |     drag_type: DragType, | ||||||
|     start_date: NaiveDate, |     start_date: NaiveDate, | ||||||
|     start_y: f64, |     start_y: f64, | ||||||
|     current_y: f64, |     current_y: f64, | ||||||
|  |     offset_y: f64, // For event moves, this is the offset from the event's top | ||||||
| } | } | ||||||
|  |  | ||||||
| #[function_component(WeekView)] | #[function_component(WeekView)] | ||||||
| @@ -119,7 +133,14 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                  |                                  | ||||||
|                                 let onmousedown = { |                                 let onmousedown = { | ||||||
|                                     let drag_state = drag_state_clone.clone(); |                                     let drag_state = drag_state_clone.clone(); | ||||||
|  |                                     let context_menus_open = props.context_menus_open; | ||||||
|  |                                     let time_increment = props.time_increment; | ||||||
|                                     Callback::from(move |e: MouseEvent| { |                                     Callback::from(move |e: MouseEvent| { | ||||||
|  |                                         // Don't start drag if any context menu is open | ||||||
|  |                                         if context_menus_open { | ||||||
|  |                                             return; | ||||||
|  |                                         } | ||||||
|  |                                          | ||||||
|                                         // Only handle left-click (button 0) |                                         // Only handle left-click (button 0) | ||||||
|                                         if e.button() != 0 { |                                         if e.button() != 0 { | ||||||
|                                             return; |                                             return; | ||||||
| @@ -130,14 +151,16 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                         let relative_y = e.layer_y() as f64; |                                         let relative_y = e.layer_y() as f64; | ||||||
|                                         let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; |                                         let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; | ||||||
|                                          |                                          | ||||||
|                                         // Snap to 15-minute increments |                                         // Snap to increment | ||||||
|                                         let snapped_y = snap_to_15_minutes(relative_y); |                                         let snapped_y = snap_to_increment(relative_y, time_increment); | ||||||
|                                          |                                          | ||||||
|                                         drag_state.set(Some(DragState { |                                         drag_state.set(Some(DragState { | ||||||
|                                             is_dragging: true, |                                             is_dragging: true, | ||||||
|  |                                             drag_type: DragType::CreateEvent, | ||||||
|                                             start_date: date_for_drag, |                                             start_date: date_for_drag, | ||||||
|                                             start_y: snapped_y, |                                             start_y: snapped_y, | ||||||
|                                             current_y: snapped_y, |                                             current_y: snapped_y, | ||||||
|  |                                             offset_y: 0.0, | ||||||
|                                         })); |                                         })); | ||||||
|                                         e.prevent_default(); |                                         e.prevent_default(); | ||||||
|                                     }) |                                     }) | ||||||
| @@ -145,6 +168,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                  |                                  | ||||||
|                                 let onmousemove = { |                                 let onmousemove = { | ||||||
|                                     let drag_state = drag_state_clone.clone(); |                                     let drag_state = drag_state_clone.clone(); | ||||||
|  |                                     let time_increment = props.time_increment; | ||||||
|                                     Callback::from(move |e: MouseEvent| { |                                     Callback::from(move |e: MouseEvent| { | ||||||
|                                         if let Some(mut current_drag) = (*drag_state).clone() { |                                         if let Some(mut current_drag) = (*drag_state).clone() { | ||||||
|                                             if current_drag.is_dragging { |                                             if current_drag.is_dragging { | ||||||
| @@ -152,8 +176,8 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                 let relative_y = e.layer_y() as f64; |                                                 let relative_y = e.layer_y() as f64; | ||||||
|                                                 let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; |                                                 let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; | ||||||
|                                                  |                                                  | ||||||
|                                                 // Snap to 15-minute increments |                                                 // Snap to increment | ||||||
|                                                 let snapped_y = snap_to_15_minutes(relative_y); |                                                 let snapped_y = snap_to_increment(relative_y, time_increment); | ||||||
|                                                  |                                                  | ||||||
|                                                 current_drag.current_y = snapped_y; |                                                 current_drag.current_y = snapped_y; | ||||||
|                                                 drag_state.set(Some(current_drag)); |                                                 drag_state.set(Some(current_drag)); | ||||||
| @@ -165,9 +189,12 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                 let onmouseup = { |                                 let onmouseup = { | ||||||
|                                     let drag_state = drag_state_clone.clone(); |                                     let drag_state = drag_state_clone.clone(); | ||||||
|                                     let on_create_event = props.on_create_event.clone(); |                                     let on_create_event = props.on_create_event.clone(); | ||||||
|  |                                     let on_event_update = props.on_event_update.clone(); | ||||||
|                                     Callback::from(move |_e: MouseEvent| { |                                     Callback::from(move |_e: MouseEvent| { | ||||||
|                                         if let Some(current_drag) = (*drag_state).clone() { |                                         if let Some(current_drag) = (*drag_state).clone() { | ||||||
|                                             if current_drag.is_dragging { |                                             if current_drag.is_dragging { | ||||||
|  |                                                 match ¤t_drag.drag_type { | ||||||
|  |                                                     DragType::CreateEvent => { | ||||||
|                                                         // Calculate start and end times |                                                         // Calculate start and end times | ||||||
|                                                         let start_time = pixels_to_time(current_drag.start_y); |                                                         let start_time = pixels_to_time(current_drag.start_y); | ||||||
|                                                         let end_time = pixels_to_time(current_drag.current_y); |                                                         let end_time = pixels_to_time(current_drag.current_y); | ||||||
| @@ -192,6 +219,26 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                         if let Some(callback) = &on_create_event { |                                                         if let Some(callback) = &on_create_event { | ||||||
|                                                             callback.emit((current_drag.start_date, start_datetime, end_datetime)); |                                                             callback.emit((current_drag.start_date, start_datetime, end_datetime)); | ||||||
|                                                         } |                                                         } | ||||||
|  |                                                     }, | ||||||
|  |                                                     DragType::MoveEvent(event) => { | ||||||
|  |                                                         // Calculate new start time based on drag position | ||||||
|  |                                                         let new_start_time = pixels_to_time(current_drag.current_y); | ||||||
|  |                                                          | ||||||
|  |                                                         // Calculate duration from original event | ||||||
|  |                                                         let original_duration = if let Some(end) = event.end { | ||||||
|  |                                                             end.signed_duration_since(event.start) | ||||||
|  |                                                         } else { | ||||||
|  |                                                             chrono::Duration::hours(1) // Default 1 hour | ||||||
|  |                                                         }; | ||||||
|  |                                                          | ||||||
|  |                                                         let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time); | ||||||
|  |                                                         let new_end_datetime = new_start_datetime + original_duration; | ||||||
|  |                                                          | ||||||
|  |                                                         if let Some(callback) = &on_event_update { | ||||||
|  |                                                             callback.emit((event.clone(), new_start_datetime, new_end_datetime)); | ||||||
|  |                                                         } | ||||||
|  |                                                     } | ||||||
|  |                                                 } | ||||||
|                                                  |                                                  | ||||||
|                                                 drag_state.set(None); |                                                 drag_state.set(None); | ||||||
|                                             } |                                             } | ||||||
| @@ -206,7 +253,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                         {onmousemove} |                                         {onmousemove} | ||||||
|                                         {onmouseup} |                                         {onmouseup} | ||||||
|                                     > |                                     > | ||||||
|                                         // Time slot backgrounds - 24 full hour slots + 1 boundary slot |                                         // Time slot backgrounds - 24 hour slots to represent full day | ||||||
|                                         { |                                         { | ||||||
|                                             (0..24).map(|_hour| { |                                             (0..24).map(|_hour| { | ||||||
|                                                 html! { |                                                 html! { | ||||||
| @@ -217,8 +264,11 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                 } |                                                 } | ||||||
|                                             }).collect::<Html>() |                                             }).collect::<Html>() | ||||||
|                                         } |                                         } | ||||||
|                                         // Final boundary slot to match the final time label |                                         // Final boundary slot to complete the 24-hour visual grid - make it interactive like other slots | ||||||
|                                         <div class="time-slot boundary-slot"></div> |                                         <div class="time-slot boundary-slot"> | ||||||
|  |                                             <div class="time-slot-half"></div> | ||||||
|  |                                             <div class="time-slot-half"></div> | ||||||
|  |                                         </div> | ||||||
|                                          |                                          | ||||||
|                                         // Events positioned absolutely based on their actual times |                                         // Events positioned absolutely based on their actual times | ||||||
|                                         <div class="events-container"> |                                         <div class="events-container"> | ||||||
| @@ -244,8 +294,41 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                     }; |                                                     }; | ||||||
|                                                      |                                                      | ||||||
|                                                     let onmousedown_event = { |                                                     let onmousedown_event = { | ||||||
|  |                                                         let drag_state = drag_state.clone(); | ||||||
|  |                                                         let event_for_drag = event.clone(); | ||||||
|  |                                                         let date_for_drag = *date; | ||||||
|  |                                                         let time_increment = props.time_increment; | ||||||
|                                                         Callback::from(move |e: MouseEvent| { |                                                         Callback::from(move |e: MouseEvent| { | ||||||
|                                                             e.stop_propagation(); // Prevent drag-to-create from starting on event clicks |                                                             e.stop_propagation(); // Prevent drag-to-create from starting on event clicks | ||||||
|  |                                                              | ||||||
|  |                                                             // Only handle left-click (button 0) | ||||||
|  |                                                             if e.button() != 0 { | ||||||
|  |                                                                 return; | ||||||
|  |                                                             } | ||||||
|  |                                                              | ||||||
|  |                                                             // Calculate Y position relative to the day column | ||||||
|  |                                                             let relative_y = e.layer_y() as f64; | ||||||
|  |                                                             let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; | ||||||
|  |                                                              | ||||||
|  |                                                             // Get event's current position | ||||||
|  |                                                             let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag); | ||||||
|  |                                                             let event_start_pixels = event_start_pixels as f64; | ||||||
|  |                                                              | ||||||
|  |                                                             // Calculate offset from the top of the event | ||||||
|  |                                                             let offset_y = relative_y - event_start_pixels; | ||||||
|  |                                                              | ||||||
|  |                                                             // Snap to increment | ||||||
|  |                                                             let snapped_y = snap_to_increment(relative_y, time_increment); | ||||||
|  |                                                              | ||||||
|  |                                                             drag_state.set(Some(DragState { | ||||||
|  |                                                                 is_dragging: true, | ||||||
|  |                                                                 drag_type: DragType::MoveEvent(event_for_drag.clone()), | ||||||
|  |                                                                 start_date: date_for_drag, | ||||||
|  |                                                                 start_y: snapped_y, | ||||||
|  |                                                                 current_y: snapped_y, | ||||||
|  |                                                                 offset_y, | ||||||
|  |                                                             })); | ||||||
|  |                                                             e.prevent_default(); | ||||||
|                                                         }) |                                                         }) | ||||||
|                                                     }; |                                                     }; | ||||||
|                                                      |                                                      | ||||||
| @@ -294,6 +377,21 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                         } |                                                         } | ||||||
|                                                     }; |                                                     }; | ||||||
|                                                      |                                                      | ||||||
|  |                                                     // Check if this event is currently being dragged | ||||||
|  |                                                     let is_being_dragged = if let Some(drag) = (*drag_state).clone() { | ||||||
|  |                                                         if let DragType::MoveEvent(dragged_event) = &drag.drag_type { | ||||||
|  |                                                             dragged_event.uid == event.uid && drag.is_dragging | ||||||
|  |                                                         } else { | ||||||
|  |                                                             false | ||||||
|  |                                                         } | ||||||
|  |                                                     } else { | ||||||
|  |                                                         false | ||||||
|  |                                                     }; | ||||||
|  |                                                      | ||||||
|  |                                                     if is_being_dragged { | ||||||
|  |                                                         // Hide the original event while being dragged | ||||||
|  |                                                         Some(html! {}) | ||||||
|  |                                                     } else { | ||||||
|                                                         Some(html! { |                                                         Some(html! { | ||||||
|                                                             <div  |                                                             <div  | ||||||
|                                                                 class={classes!( |                                                                 class={classes!( | ||||||
| @@ -319,6 +417,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                                 }} |                                                                 }} | ||||||
|                                                             </div> |                                                             </div> | ||||||
|                                                         }) |                                                         }) | ||||||
|  |                                                     } | ||||||
|                                                 }).collect::<Html>() |                                                 }).collect::<Html>() | ||||||
|                                             } |                                             } | ||||||
|                                         </div> |                                         </div> | ||||||
| @@ -327,6 +426,8 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                         { |                                         { | ||||||
|                                             if let Some(drag) = (*drag_state).clone() { |                                             if let Some(drag) = (*drag_state).clone() { | ||||||
|                                                 if drag.is_dragging && drag.start_date == *date { |                                                 if drag.is_dragging && drag.start_date == *date { | ||||||
|  |                                                     match &drag.drag_type { | ||||||
|  |                                                         DragType::CreateEvent => { | ||||||
|                                                             let start_y = drag.start_y.min(drag.current_y); |                                                             let start_y = drag.start_y.min(drag.current_y); | ||||||
|                                                             let end_y = drag.start_y.max(drag.current_y); |                                                             let end_y = drag.start_y.max(drag.current_y); | ||||||
|                                                             let height = (drag.current_y - drag.start_y).abs().max(20.0); |                                                             let height = (drag.current_y - drag.start_y).abs().max(20.0); | ||||||
| @@ -343,6 +444,31 @@ pub fn week_view(props: &WeekViewProps) -> Html { | |||||||
|                                                                     {format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))} |                                                                     {format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))} | ||||||
|                                                                 </div> |                                                                 </div> | ||||||
|                                                             } |                                                             } | ||||||
|  |                                                         }, | ||||||
|  |                                                         DragType::MoveEvent(event) => { | ||||||
|  |                                                             // Show the event being moved at its new position | ||||||
|  |                                                             let new_start_time = pixels_to_time(drag.current_y); | ||||||
|  |                                                             let original_duration = if let Some(end) = event.end { | ||||||
|  |                                                                 end.signed_duration_since(event.start) | ||||||
|  |                                                             } else { | ||||||
|  |                                                                 chrono::Duration::hours(1) | ||||||
|  |                                                             }; | ||||||
|  |                                                             let duration_pixels = (original_duration.num_minutes() as f64).max(20.0); | ||||||
|  |                                                             let new_end_time = new_start_time + original_duration; | ||||||
|  |                                                              | ||||||
|  |                                                             let event_color = get_event_color(event); | ||||||
|  |                                                              | ||||||
|  |                                                             html! { | ||||||
|  |                                                                 <div | ||||||
|  |                                                                     class="temp-event-box moving-event" | ||||||
|  |                                                                     style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", drag.current_y, duration_pixels, event_color)} | ||||||
|  |                                                                 > | ||||||
|  |                                                                     <div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div> | ||||||
|  |                                                                     <div class="event-time">{format!("{} - {}", new_start_time.format("%I:%M %p"), new_end_time.format("%I:%M %p"))}</div> | ||||||
|  |                                                                 </div> | ||||||
|  |                                                             } | ||||||
|  |                                                         } | ||||||
|  |                                                     } | ||||||
|                                                 } else { |                                                 } else { | ||||||
|                                                     html! {} |                                                     html! {} | ||||||
|                                                 } |                                                 } | ||||||
| @@ -390,9 +516,9 @@ fn get_weekday_name(weekday: Weekday) -> &'static str { | |||||||
| // Calculate the pixel position of an event based on its time | // Calculate the pixel position of an event based on its time | ||||||
| // Each hour is 60px, so we convert time to pixels | // Each hour is 60px, so we convert time to pixels | ||||||
| // Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes) | // Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes) | ||||||
| fn snap_to_15_minutes(pixels: f64) -> f64 { | fn snap_to_increment(pixels: f64, increment: u32) -> f64 { | ||||||
|     let increment = 15.0; // 15px = 15 minutes |     let increment_px = increment as f64; // Convert to pixels (1px = 1 minute) | ||||||
|     (pixels / increment).round() * increment |     (pixels / increment_px).round() * increment_px | ||||||
| } | } | ||||||
|  |  | ||||||
| // Convert pixel position to time (inverse of time to pixels) | // Convert pixel position to time (inverse of time to pixels) | ||||||
| @@ -402,7 +528,12 @@ fn pixels_to_time(pixels: f64) -> NaiveTime { | |||||||
|     let hours = (total_minutes / 60.0) as u32; |     let hours = (total_minutes / 60.0) as u32; | ||||||
|     let minutes = (total_minutes % 60.0) as u32; |     let minutes = (total_minutes % 60.0) as u32; | ||||||
|      |      | ||||||
|     // Clamp to valid time range |     // Handle midnight boundary - if we're at exactly 1440 pixels (24:00), return midnight | ||||||
|  |     if total_minutes >= 1440.0 { | ||||||
|  |         return NaiveTime::from_hms_opt(0, 0, 0).unwrap(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Clamp to valid time range for within-day times | ||||||
|     let hours = hours.min(23); |     let hours = hours.min(23); | ||||||
|     let minutes = minutes.min(59); |     let minutes = minutes.min(59); | ||||||
|      |      | ||||||
|   | |||||||
							
								
								
									
										85
									
								
								styles.css
									
									
									
									
									
								
							
							
						
						
									
										85
									
								
								styles.css
									
									
									
									
									
								
							| @@ -410,12 +410,38 @@ body { | |||||||
|     transform: translateX(-50%); |     transform: translateX(-50%); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .header-left { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     gap: 8px; | ||||||
|  | } | ||||||
|  |  | ||||||
| .header-right { | .header-right { | ||||||
|     display: flex; |     display: flex; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     gap: 0.5rem; |     gap: 0.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .time-increment-button { | ||||||
|  |     background: rgba(255,255,255,0.2); | ||||||
|  |     border: none; | ||||||
|  |     color: white; | ||||||
|  |     font-size: 14px; | ||||||
|  |     font-weight: bold; | ||||||
|  |     width: 40px; | ||||||
|  |     height: 40px; | ||||||
|  |     border-radius: 50%; | ||||||
|  |     cursor: pointer; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     transition: background-color 0.2s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-increment-button:hover { | ||||||
|  |     background: rgba(255,255,255,0.3); | ||||||
|  | } | ||||||
|  |  | ||||||
| .nav-button { | .nav-button { | ||||||
|     background: rgba(255,255,255,0.2); |     background: rgba(255,255,255,0.2); | ||||||
|     border: none; |     border: none; | ||||||
| @@ -458,8 +484,10 @@ body { | |||||||
| .calendar-grid { | .calendar-grid { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-columns: repeat(7, 1fr); |     grid-template-columns: repeat(7, 1fr); | ||||||
|  |     grid-template-rows: auto repeat(6, 1fr); | ||||||
|     flex: 1; |     flex: 1; | ||||||
|     background: white; |     background: white; | ||||||
|  |     gap: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| /* Week View Container */ | /* Week View Container */ | ||||||
| @@ -668,6 +696,37 @@ body { | |||||||
|     user-select: none; |     user-select: none; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* Moving event during drag */ | ||||||
|  | .temp-event-box.moving-event { | ||||||
|  |     background: inherit; /* Use the event's actual color */ | ||||||
|  |     border: 2px solid rgba(255, 255, 255, 0.8); | ||||||
|  |     color: white; | ||||||
|  |     flex-direction: column; | ||||||
|  |     align-items: flex-start; | ||||||
|  |     justify-content: flex-start; | ||||||
|  |     text-align: left; | ||||||
|  |     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); | ||||||
|  |     transform: scale(1.02); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .temp-event-box.moving-event .event-title { | ||||||
|  |     font-weight: 600; | ||||||
|  |     margin-bottom: 2px; | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |     white-space: nowrap; | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .temp-event-box.moving-event .event-time { | ||||||
|  |     font-size: 0.65rem; | ||||||
|  |     opacity: 0.9; | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |     white-space: nowrap; | ||||||
|  |     width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
| .week-event .event-title { | .week-event .event-title { | ||||||
|     font-weight: 600; |     font-weight: 600; | ||||||
|     margin-bottom: 2px; |     margin-bottom: 2px; | ||||||
| @@ -717,12 +776,12 @@ body { | |||||||
| .calendar-day { | .calendar-day { | ||||||
|     border: 1px solid #f0f0f0; |     border: 1px solid #f0f0f0; | ||||||
|     padding: 0.75rem; |     padding: 0.75rem; | ||||||
|     min-height: 100px; |  | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|     transition: background-color 0.2s; |     transition: background-color 0.2s; | ||||||
|     position: relative; |     position: relative; | ||||||
|  |     overflow: hidden; | ||||||
| } | } | ||||||
|  |  | ||||||
| .calendar-day:hover { | .calendar-day:hover { | ||||||
| @@ -782,6 +841,14 @@ body { | |||||||
|     color: #1976d2; |     color: #1976d2; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .day-events { | ||||||
|  |     flex: 1; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     gap: 2px; | ||||||
|  |     overflow: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
| .event-indicators { | .event-indicators { | ||||||
|     flex: 1; |     flex: 1; | ||||||
|     display: flex; |     display: flex; | ||||||
| @@ -789,6 +856,22 @@ body { | |||||||
|     gap: 2px; |     gap: 2px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .more-events-indicator { | ||||||
|  |     font-size: 0.7rem; | ||||||
|  |     color: #666; | ||||||
|  |     font-weight: 500; | ||||||
|  |     padding: 2px 4px; | ||||||
|  |     text-align: center; | ||||||
|  |     background: #f5f5f5; | ||||||
|  |     border-radius: 3px; | ||||||
|  |     cursor: pointer; | ||||||
|  |     transition: background-color 0.2s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .more-events-indicator:hover { | ||||||
|  |     background: #e0e0e0; | ||||||
|  | } | ||||||
|  |  | ||||||
| .event-box { | .event-box { | ||||||
|     /* Background color will be set inline via style attribute */ |     /* Background color will be set inline via style attribute */ | ||||||
|     color: white; |     color: white; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user