Compare commits
	
		
			9 Commits
		
	
	
		
			197157cecb
			...
			edb216347d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | edb216347d | ||
|   | 508c4f129f | ||
|   | 1c0140292f | ||
|   | 53815c4814 | ||
|   | df714a43a2 | ||
|   | a8bb2c8164 | ||
|   | 5d0628878b | ||
|   | dacc18fe5d | ||
|   | 9ab6377d16 | 
							
								
								
									
										200
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										200
									
								
								src/app.rs
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ use yew::prelude::*; | |||||||
| use yew_router::prelude::*; | use yew_router::prelude::*; | ||||||
| use gloo_storage::{LocalStorage, Storage}; | use gloo_storage::{LocalStorage, Storage}; | ||||||
| use web_sys::MouseEvent; | use web_sys::MouseEvent; | ||||||
| use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction}; | use crate::components::{Sidebar, ViewMode, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction}; | ||||||
| use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}}; | use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}}; | ||||||
| use chrono::NaiveDate; | use chrono::NaiveDate; | ||||||
|  |  | ||||||
| @@ -28,6 +28,19 @@ pub fn App() -> Html { | |||||||
|     let create_event_modal_open = use_state(|| false); |     let create_event_modal_open = use_state(|| false); | ||||||
|     let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None }); |     let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None }); | ||||||
|      |      | ||||||
|  |     // Calendar view state - load from localStorage if available | ||||||
|  |     let current_view = use_state(|| { | ||||||
|  |         // Try to load saved view mode from localStorage | ||||||
|  |         if let Ok(saved_view) = LocalStorage::get::<String>("calendar_view_mode") { | ||||||
|  |             match saved_view.as_str() { | ||||||
|  |                 "week" => ViewMode::Week, | ||||||
|  |                 _ => ViewMode::Month, | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             ViewMode::Month // Default to month view | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |      | ||||||
|     let available_colors = [ |     let available_colors = [ | ||||||
|         "#3B82F6", "#10B981", "#F59E0B", "#EF4444",  |         "#3B82F6", "#10B981", "#F59E0B", "#EF4444",  | ||||||
|         "#8B5CF6", "#06B6D4", "#84CC16", "#F97316", |         "#8B5CF6", "#06B6D4", "#84CC16", "#F97316", | ||||||
| @@ -52,6 +65,21 @@ pub fn App() -> Html { | |||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     let on_view_change = { | ||||||
|  |         let current_view = current_view.clone(); | ||||||
|  |         Callback::from(move |new_view: ViewMode| { | ||||||
|  |             // Save view mode to localStorage | ||||||
|  |             let view_string = match new_view { | ||||||
|  |                 ViewMode::Month => "month", | ||||||
|  |                 ViewMode::Week => "week", | ||||||
|  |             }; | ||||||
|  |             let _ = LocalStorage::set("calendar_view_mode", view_string); | ||||||
|  |              | ||||||
|  |             // Update state | ||||||
|  |             current_view.set(new_view); | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     // Fetch user info when token is available |     // Fetch user info when token is available | ||||||
|     { |     { | ||||||
|         let user_info = user_info.clone(); |         let user_info = user_info.clone(); | ||||||
| @@ -215,11 +243,18 @@ pub fn App() -> Html { | |||||||
|                         String::new() |                         String::new() | ||||||
|                     }; |                     }; | ||||||
|                      |                      | ||||||
|                     // Format date and time strings |                     // Convert local times to UTC for backend storage | ||||||
|                     let start_date = event_data.start_date.format("%Y-%m-%d").to_string(); |                     let start_local = event_data.start_date.and_time(event_data.start_time); | ||||||
|                     let start_time = event_data.start_time.format("%H:%M").to_string(); |                     let end_local = event_data.end_date.and_time(event_data.end_time); | ||||||
|                     let end_date = event_data.end_date.format("%Y-%m-%d").to_string(); |                      | ||||||
|                     let end_time = event_data.end_time.format("%H:%M").to_string(); |                     let start_utc = start_local.and_local_timezone(chrono::Local).unwrap().to_utc(); | ||||||
|  |                     let end_utc = end_local.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 enums to strings for backend |                     // Convert enums to strings for backend | ||||||
|                     let status_str = match event_data.status { |                     let status_str = match event_data.status { | ||||||
| @@ -354,6 +389,8 @@ pub fn App() -> Html { | |||||||
|                                     on_color_picker_toggle={on_color_picker_toggle} |                                     on_color_picker_toggle={on_color_picker_toggle} | ||||||
|                                     available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()} |                                     available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()} | ||||||
|                                     on_calendar_context_menu={on_calendar_context_menu} |                                     on_calendar_context_menu={on_calendar_context_menu} | ||||||
|  |                                     current_view={(*current_view).clone()} | ||||||
|  |                                     on_view_change={on_view_change} | ||||||
|                                 /> |                                 /> | ||||||
|                                 <main class="app-main"> |                                 <main class="app-main"> | ||||||
|                                     <RouteHandler  |                                     <RouteHandler  | ||||||
| @@ -362,6 +399,8 @@ 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())} | ||||||
|  |                                         view={(*current_view).clone()} | ||||||
|  |                                         on_create_event_request={Some(on_event_create.clone())} | ||||||
|                                     /> |                                     /> | ||||||
|                                 </main> |                                 </main> | ||||||
|                             </> |                             </> | ||||||
| @@ -375,6 +414,7 @@ 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_create_event_request={Some(on_event_create.clone())} | ||||||
|                                 /> |                                 /> | ||||||
|                             </div> |                             </div> | ||||||
|                         } |                         } | ||||||
| @@ -620,11 +660,18 @@ pub fn App() -> Html { | |||||||
|                                         String::new() |                                         String::new() | ||||||
|                                     }; |                                     }; | ||||||
|                                      |                                      | ||||||
|                                     // Format date and time strings |                                     // Convert local times to UTC for backend storage | ||||||
|                                     let start_date = updated_data.start_date.format("%Y-%m-%d").to_string(); |                                     let start_local = updated_data.start_date.and_time(updated_data.start_time); | ||||||
|                                     let start_time = updated_data.start_time.format("%H:%M").to_string(); |                                     let end_local = updated_data.end_date.and_time(updated_data.end_time); | ||||||
|                                     let end_date = updated_data.end_date.format("%Y-%m-%d").to_string(); |                                      | ||||||
|                                     let end_time = updated_data.end_time.format("%H:%M").to_string(); |                                     let start_utc = start_local.and_local_timezone(chrono::Local).unwrap().to_utc(); | ||||||
|  |                                     let end_utc = end_local.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 enums to strings for backend |                                     // Convert enums to strings for backend | ||||||
|                                     let status_str = match updated_data.status { |                                     let status_str = match updated_data.status { | ||||||
| @@ -658,37 +705,108 @@ pub fn App() -> Html { | |||||||
|                                         _ => "none", |                                         _ => "none", | ||||||
|                                     }.to_string(); |                                     }.to_string(); | ||||||
|  |  | ||||||
|                                     match calendar_service.update_event( |                                     // Check if the calendar has changed | ||||||
|                                         &token, |                                     let calendar_changed = original_event.calendar_path.as_ref() != updated_data.selected_calendar.as_ref(); | ||||||
|                                         &password, |                                      | ||||||
|                                         original_event.uid, |                                     if calendar_changed { | ||||||
|                                         updated_data.title, |                                         // Calendar changed - need to delete from original and create in new | ||||||
|                                         updated_data.description, |                                         web_sys::console::log_1(&"Calendar changed - performing delete + create".into()); | ||||||
|                                         start_date, |                                          | ||||||
|                                         start_time, |                                         // First delete from original calendar | ||||||
|                                         end_date, |                                         if let Some(original_calendar_path) = &original_event.calendar_path { | ||||||
|                                         end_time, |                                             if let Some(event_href) = &original_event.href { | ||||||
|                                         updated_data.location, |                                                 match calendar_service.delete_event( | ||||||
|                                         updated_data.all_day, |                                                     &token, | ||||||
|                                         status_str, |                                                     &password, | ||||||
|                                         class_str, |                                                     original_calendar_path.clone(), | ||||||
|                                         updated_data.priority, |                                                     event_href.clone(), | ||||||
|                                         updated_data.organizer, |                                                     "single".to_string(), // delete single occurrence  | ||||||
|                                         updated_data.attendees, |                                                     None | ||||||
|                                         updated_data.categories, |                                                 ).await { | ||||||
|                                         reminder_str, |                                                     Ok(_) => { | ||||||
|                                         recurrence_str, |                                                         web_sys::console::log_1(&"Original event deleted successfully".into()); | ||||||
|                                         updated_data.recurrence_days, |                                                          | ||||||
|                                         updated_data.selected_calendar |                                                         // Now create the event in the new calendar | ||||||
|                                     ).await { |                                                         match calendar_service.create_event( | ||||||
|                                         Ok(_) => { |                                                             &token, | ||||||
|                                             web_sys::console::log_1(&"Event updated successfully".into()); |                                                             &password, | ||||||
|                                             // Trigger a page reload to refresh events from all calendars |                                                             updated_data.title, | ||||||
|                                             web_sys::window().unwrap().location().reload().unwrap(); |                                                             updated_data.description, | ||||||
|  |                                                             start_date, | ||||||
|  |                                                             start_time, | ||||||
|  |                                                             end_date, | ||||||
|  |                                                             end_time, | ||||||
|  |                                                             updated_data.location, | ||||||
|  |                                                             updated_data.all_day, | ||||||
|  |                                                             status_str, | ||||||
|  |                                                             class_str, | ||||||
|  |                                                             updated_data.priority, | ||||||
|  |                                                             updated_data.organizer, | ||||||
|  |                                                             updated_data.attendees, | ||||||
|  |                                                             updated_data.categories, | ||||||
|  |                                                             reminder_str, | ||||||
|  |                                                             recurrence_str, | ||||||
|  |                                                             updated_data.recurrence_days, | ||||||
|  |                                                             updated_data.selected_calendar | ||||||
|  |                                                         ).await { | ||||||
|  |                                                             Ok(_) => { | ||||||
|  |                                                                 web_sys::console::log_1(&"Event moved to new calendar 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 create event in new calendar: {}", err).into()); | ||||||
|  |                                                                 web_sys::window().unwrap().alert_with_message(&format!("Failed to move event to new calendar: {}", err)).unwrap(); | ||||||
|  |                                                             } | ||||||
|  |                                                         } | ||||||
|  |                                                     } | ||||||
|  |                                                     Err(err) => { | ||||||
|  |                                                         web_sys::console::error_1(&format!("Failed to delete original event: {}", err).into()); | ||||||
|  |                                                         web_sys::window().unwrap().alert_with_message(&format!("Failed to delete original event: {}", err)).unwrap(); | ||||||
|  |                                                     } | ||||||
|  |                                                 } | ||||||
|  |                                             } else { | ||||||
|  |                                                 web_sys::console::error_1(&"Original event missing href for deletion".into()); | ||||||
|  |                                                 web_sys::window().unwrap().alert_with_message("Cannot move event - original event missing href").unwrap(); | ||||||
|  |                                             } | ||||||
|  |                                         } else { | ||||||
|  |                                             web_sys::console::error_1(&"Original event missing calendar_path for deletion".into()); | ||||||
|  |                                             web_sys::window().unwrap().alert_with_message("Cannot move event - original event missing calendar path").unwrap(); | ||||||
|                                         } |                                         } | ||||||
|                                         Err(err) => { |                                     } else { | ||||||
|                                             web_sys::console::error_1(&format!("Failed to update event: {}", err).into()); |                                         // Calendar hasn't changed - normal update | ||||||
|                                             web_sys::window().unwrap().alert_with_message(&format!("Failed to update event: {}", err)).unwrap(); |                                         match calendar_service.update_event( | ||||||
|  |                                             &token, | ||||||
|  |                                             &password, | ||||||
|  |                                             original_event.uid, | ||||||
|  |                                             updated_data.title, | ||||||
|  |                                             updated_data.description, | ||||||
|  |                                             start_date, | ||||||
|  |                                             start_time, | ||||||
|  |                                             end_date, | ||||||
|  |                                             end_time, | ||||||
|  |                                             updated_data.location, | ||||||
|  |                                             updated_data.all_day, | ||||||
|  |                                             status_str, | ||||||
|  |                                             class_str, | ||||||
|  |                                             updated_data.priority, | ||||||
|  |                                             updated_data.organizer, | ||||||
|  |                                             updated_data.attendees, | ||||||
|  |                                             updated_data.categories, | ||||||
|  |                                             reminder_str, | ||||||
|  |                                             recurrence_str, | ||||||
|  |                                             updated_data.recurrence_days, | ||||||
|  |                                             updated_data.selected_calendar | ||||||
|  |                                         ).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(); | ||||||
|  |                                             } | ||||||
|                                         } |                                         } | ||||||
|                                     } |                                     } | ||||||
|                                 }); |                                 }); | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| use yew::prelude::*; | use yew::prelude::*; | ||||||
| use chrono::{Datelike, Local, NaiveDate, Duration, Weekday}; | use chrono::{Datelike, Local, NaiveDate, Duration}; | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|  | use web_sys::MouseEvent; | ||||||
| use crate::services::calendar_service::{CalendarEvent, UserInfo}; | use crate::services::calendar_service::{CalendarEvent, UserInfo}; | ||||||
| use crate::components::EventModal; | use crate::components::{EventModal, ViewMode, CalendarHeader, MonthView, WeekView, CreateEventModal, EventCreationData}; | ||||||
| use wasm_bindgen::JsCast; |  | ||||||
| use gloo_storage::{LocalStorage, Storage}; | use gloo_storage::{LocalStorage, Storage}; | ||||||
|  |  | ||||||
| #[derive(Properties, PartialEq)] | #[derive(Properties, PartialEq)] | ||||||
| @@ -19,235 +19,200 @@ pub struct CalendarProps { | |||||||
|     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>, |     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     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] | ||||||
|  |     pub view: ViewMode, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_create_event_request: Option<Callback<EventCreationData>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[function_component] | #[function_component] | ||||||
| pub fn Calendar(props: &CalendarProps) -> Html { | pub fn Calendar(props: &CalendarProps) -> Html { | ||||||
|     let today = Local::now().date_naive(); |     let today = Local::now().date_naive(); | ||||||
|     let current_month = use_state(|| { |     // Track the currently selected date (the actual day the user has selected) | ||||||
|         // Try to load saved month from localStorage |     let selected_date = use_state(|| { | ||||||
|         if let Ok(saved_month_str) = LocalStorage::get::<String>("calendar_current_month") { |         // Try to load saved selected date from localStorage | ||||||
|             if let Ok(saved_month) = NaiveDate::parse_from_str(&saved_month_str, "%Y-%m-%d") { |         if let Ok(saved_date_str) = LocalStorage::get::<String>("calendar_selected_date") { | ||||||
|                 // Return the first day of the saved month |             if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { | ||||||
|                 saved_month.with_day(1).unwrap_or(today) |                 saved_date | ||||||
|             } else { |             } else { | ||||||
|                 today |                 today | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             today |             // Check for old key for backward compatibility | ||||||
|         } |             if let Ok(saved_date_str) = LocalStorage::get::<String>("calendar_current_month") { | ||||||
|     }); |                 if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { | ||||||
|     let selected_day = use_state(|| today); |                     saved_date | ||||||
|     let selected_event = use_state(|| None::<CalendarEvent>); |                 } else { | ||||||
|      |                     today | ||||||
|     // 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(); |  | ||||||
|                 } |                 } | ||||||
|  |             } else { | ||||||
|  |                 today | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         // Default color if no match found |     }); | ||||||
|         "#3B82F6".to_string() |  | ||||||
|     }; |  | ||||||
|      |      | ||||||
|     let first_day_of_month = current_month.with_day(1).unwrap(); |     // Track the display date (what to show in the view) | ||||||
|     let days_in_month = get_days_in_month(*current_month); |     let current_date = use_state(|| { | ||||||
|     let first_weekday = first_day_of_month.weekday(); |         match props.view { | ||||||
|     let days_from_prev_month = get_days_from_previous_month(*current_month, first_weekday); |             ViewMode::Month => selected_date.with_day(1).unwrap_or(*selected_date), | ||||||
|  |             ViewMode::Week => *selected_date, | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |     let selected_event = use_state(|| None::<CalendarEvent>); | ||||||
|      |      | ||||||
|     let prev_month = { |     // State for create event modal | ||||||
|         let current_month = current_month.clone(); |     let show_create_modal = use_state(|| false); | ||||||
|         Callback::from(move |_| { |     let create_event_data = use_state(|| None::<(chrono::NaiveDate, chrono::NaiveTime, chrono::NaiveTime)>); | ||||||
|             let prev = *current_month - Duration::days(1); |  | ||||||
|             let first_of_prev = prev.with_day(1).unwrap(); |  | ||||||
|             current_month.set(first_of_prev); |  | ||||||
|             // Save to localStorage |  | ||||||
|             let _ = LocalStorage::set("calendar_current_month", first_of_prev.format("%Y-%m-%d").to_string()); |  | ||||||
|         }) |  | ||||||
|     }; |  | ||||||
|      |      | ||||||
|     let next_month = { |     // Handle view mode changes - adjust current_date format when switching between month/week | ||||||
|         let current_month = current_month.clone(); |     { | ||||||
|         Callback::from(move |_| { |         let current_date = current_date.clone(); | ||||||
|             let next = if current_month.month() == 12 { |         let selected_date = selected_date.clone(); | ||||||
|                 NaiveDate::from_ymd_opt(current_month.year() + 1, 1, 1).unwrap() |         let view = props.view.clone(); | ||||||
|             } else { |         use_effect_with(view, move |view_mode| { | ||||||
|                 NaiveDate::from_ymd_opt(current_month.year(), current_month.month() + 1, 1).unwrap() |             let selected = *selected_date; | ||||||
|  |             let new_display_date = match view_mode { | ||||||
|  |                 ViewMode::Month => selected.with_day(1).unwrap_or(selected), | ||||||
|  |                 ViewMode::Week => selected, // Show the week containing the selected date | ||||||
|             }; |             }; | ||||||
|             current_month.set(next); |             current_date.set(new_display_date); | ||||||
|             // Save to localStorage |             || {} | ||||||
|             let _ = LocalStorage::set("calendar_current_month", next.format("%Y-%m-%d").to_string()); |         }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     let on_prev = { | ||||||
|  |         let current_date = current_date.clone(); | ||||||
|  |         let selected_date = selected_date.clone(); | ||||||
|  |         let view = props.view.clone(); | ||||||
|  |         Callback::from(move |_: MouseEvent| { | ||||||
|  |             let (new_selected, new_display) = match view { | ||||||
|  |                 ViewMode::Month => { | ||||||
|  |                     // Go to previous month, select the 1st day | ||||||
|  |                     let prev_month = *current_date - Duration::days(1); | ||||||
|  |                     let first_of_prev = prev_month.with_day(1).unwrap(); | ||||||
|  |                     (first_of_prev, first_of_prev) | ||||||
|  |                 }, | ||||||
|  |                 ViewMode::Week => { | ||||||
|  |                     // Go to previous week | ||||||
|  |                     let prev_week = *selected_date - Duration::weeks(1); | ||||||
|  |                     (prev_week, prev_week) | ||||||
|  |                 }, | ||||||
|  |             }; | ||||||
|  |             selected_date.set(new_selected); | ||||||
|  |             current_date.set(new_display); | ||||||
|  |             let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); | ||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
|     let go_to_today = { |     let on_next = { | ||||||
|         let current_month = current_month.clone(); |         let current_date = current_date.clone(); | ||||||
|  |         let selected_date = selected_date.clone(); | ||||||
|  |         let view = props.view.clone(); | ||||||
|  |         Callback::from(move |_: MouseEvent| { | ||||||
|  |             let (new_selected, new_display) = match view { | ||||||
|  |                 ViewMode::Month => { | ||||||
|  |                     // Go to next month, select the 1st day | ||||||
|  |                     let next_month = if current_date.month() == 12 { | ||||||
|  |                         NaiveDate::from_ymd_opt(current_date.year() + 1, 1, 1).unwrap() | ||||||
|  |                     } else { | ||||||
|  |                         NaiveDate::from_ymd_opt(current_date.year(), current_date.month() + 1, 1).unwrap() | ||||||
|  |                     }; | ||||||
|  |                     (next_month, next_month) | ||||||
|  |                 }, | ||||||
|  |                 ViewMode::Week => { | ||||||
|  |                     // Go to next week | ||||||
|  |                     let next_week = *selected_date + Duration::weeks(1); | ||||||
|  |                     (next_week, next_week) | ||||||
|  |                 }, | ||||||
|  |             }; | ||||||
|  |             selected_date.set(new_selected); | ||||||
|  |             current_date.set(new_display); | ||||||
|  |             let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let on_today = { | ||||||
|  |         let current_date = current_date.clone(); | ||||||
|  |         let selected_date = selected_date.clone(); | ||||||
|  |         let view = props.view.clone(); | ||||||
|         Callback::from(move |_| { |         Callback::from(move |_| { | ||||||
|             let today = Local::now().date_naive(); |             let today = Local::now().date_naive(); | ||||||
|             let first_of_today_month = today.with_day(1).unwrap(); |             let (new_selected, new_display) = match view { | ||||||
|             current_month.set(first_of_today_month); |                 ViewMode::Month => { | ||||||
|             // Save to localStorage |                     let first_of_today = today.with_day(1).unwrap(); | ||||||
|             let _ = LocalStorage::set("calendar_current_month", first_of_today_month.format("%Y-%m-%d").to_string()); |                     (today, first_of_today) // Select today, but display the month | ||||||
|  |                 }, | ||||||
|  |                 ViewMode::Week => (today, today), // Select and display today | ||||||
|  |             }; | ||||||
|  |             selected_date.set(new_selected); | ||||||
|  |             current_date.set(new_display); | ||||||
|  |             let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     // Handle drag-to-create event | ||||||
|  |     let on_create_event = { | ||||||
|  |         let show_create_modal = show_create_modal.clone(); | ||||||
|  |         let create_event_data = create_event_data.clone(); | ||||||
|  |         Callback::from(move |(_date, start_datetime, end_datetime): (NaiveDate, chrono::NaiveDateTime, chrono::NaiveDateTime)| { | ||||||
|  |             // For drag-to-create, we don't need the temporary event approach | ||||||
|  |             // Instead, just pass the local times directly via initial_time props | ||||||
|  |             create_event_data.set(Some((start_datetime.date(), start_datetime.time(), end_datetime.time()))); | ||||||
|  |             show_create_modal.set(true); | ||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     html! { |     html! { | ||||||
|         <div class="calendar"> |         <div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}> | ||||||
|             <div class="calendar-header"> |             <CalendarHeader  | ||||||
|                 <button class="nav-button" onclick={prev_month}>{"‹"}</button> |                 current_date={*current_date} | ||||||
|                 <h2 class="month-year">{format!("{} {}", get_month_name(current_month.month()), current_month.year())}</h2> |                 view_mode={props.view.clone()} | ||||||
|                 <div class="header-right"> |                 on_prev={on_prev} | ||||||
|                     <button class="today-button" onclick={go_to_today}>{"Today"}</button> |                 on_next={on_next} | ||||||
|                     <button class="nav-button" onclick={next_month}>{"›"}</button> |                 on_today={on_today} | ||||||
|                 </div> |             /> | ||||||
|             </div> |  | ||||||
|              |              | ||||||
|             <div class="calendar-grid"> |             { | ||||||
|                 // Weekday headers |                 match props.view { | ||||||
|                 <div class="weekday-header">{"Sun"}</div> |                     ViewMode::Month => { | ||||||
|                 <div class="weekday-header">{"Mon"}</div> |                         let on_day_select = { | ||||||
|                 <div class="weekday-header">{"Tue"}</div> |                             let selected_date = selected_date.clone(); | ||||||
|                 <div class="weekday-header">{"Wed"}</div> |                             Callback::from(move |date: NaiveDate| { | ||||||
|                 <div class="weekday-header">{"Thu"}</div> |                                 selected_date.set(date); | ||||||
|                 <div class="weekday-header">{"Fri"}</div> |                                 let _ = LocalStorage::set("calendar_selected_date", date.format("%Y-%m-%d").to_string()); | ||||||
|                 <div class="weekday-header">{"Sat"}</div> |  | ||||||
|                  |  | ||||||
|                 // Days from previous month (grayed out) |  | ||||||
|                 { |  | ||||||
|                     days_from_prev_month.iter().map(|day| { |  | ||||||
|                         html! { |  | ||||||
|                             <div class="calendar-day prev-month">{*day}</div> |  | ||||||
|                         } |  | ||||||
|                     }).collect::<Html>() |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 // Days of current month |  | ||||||
|                 { |  | ||||||
|                     (1..=days_in_month).map(|day| { |  | ||||||
|                         let date = current_month.with_day(day).unwrap(); |  | ||||||
|                         let is_today = date == today; |  | ||||||
|                         let is_selected = date == *selected_day; |  | ||||||
|                         let events = props.events.get(&date).cloned().unwrap_or_default(); |  | ||||||
|                          |  | ||||||
|                         let mut classes = vec!["calendar-day", "current-month"]; |  | ||||||
|                         if is_today { |  | ||||||
|                             classes.push("today"); |  | ||||||
|                         } |  | ||||||
|                         if is_selected { |  | ||||||
|                             classes.push("selected"); |  | ||||||
|                         } |  | ||||||
|                         if !events.is_empty() { |  | ||||||
|                             classes.push("has-events"); |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         let selected_day_clone = selected_day.clone(); |  | ||||||
|                         let on_click = Callback::from(move |_| { |  | ||||||
|                             selected_day_clone.set(date); |  | ||||||
|                         }); |  | ||||||
|  |  | ||||||
|                         let on_context_menu = { |  | ||||||
|                             let on_calendar_context_menu = props.on_calendar_context_menu.clone(); |  | ||||||
|                             Callback::from(move |e: MouseEvent| { |  | ||||||
|                                 // Only show context menu if we're not right-clicking on an event |  | ||||||
|                                 if let Some(target) = e.target() { |  | ||||||
|                                     if let Ok(element) = target.dyn_into::<web_sys::Element>() { |  | ||||||
|                                         // Check if the click is on an event box or inside one |  | ||||||
|                                         let mut current = Some(element); |  | ||||||
|                                         while let Some(el) = current { |  | ||||||
|                                             if el.class_name().contains("event-box") { |  | ||||||
|                                                 return; // Don't show calendar context menu on events |  | ||||||
|                                             } |  | ||||||
|                                             current = el.parent_element(); |  | ||||||
|                                         } |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                                  |  | ||||||
|                                 e.prevent_default(); |  | ||||||
|                                 e.stop_propagation(); |  | ||||||
|                                 if let Some(callback) = &on_calendar_context_menu { |  | ||||||
|                                     callback.emit((e, date)); |  | ||||||
|                                 } |  | ||||||
|                             }) |                             }) | ||||||
|                         }; |                         }; | ||||||
|                          |                          | ||||||
|                         html! { |                         html! { | ||||||
|                             <div class={classes!(classes)} onclick={on_click} oncontextmenu={on_context_menu}> |                             <MonthView | ||||||
|                                 <div class="day-number">{day}</div> |                                 current_month={*current_date} | ||||||
|                                 { |                                 today={today} | ||||||
|                                     if !events.is_empty() { |                                 events={props.events.clone()} | ||||||
|                                         html! { |                                 on_event_click={props.on_event_click.clone()} | ||||||
|                                             <div class="event-indicators"> |                                 refreshing_event_uid={props.refreshing_event_uid.clone()} | ||||||
|                                                 { |                                 user_info={props.user_info.clone()} | ||||||
|                                                     events.iter().take(2).map(|event| { |                                 on_event_context_menu={props.on_event_context_menu.clone()} | ||||||
|                                                         let event_clone = event.clone(); |                                 on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||||
|                                                         let selected_event_clone = selected_event.clone(); |                                 selected_date={Some(*selected_date)} | ||||||
|                                                         let on_event_click = props.on_event_click.clone(); |                                 on_day_select={Some(on_day_select)} | ||||||
|                                                         let event_click = Callback::from(move |e: MouseEvent| { |                             /> | ||||||
|                                                             e.stop_propagation(); // Prevent day selection |  | ||||||
|                                                             on_event_click.emit(event_clone.clone()); |  | ||||||
|                                                             selected_event_clone.set(Some(event_clone.clone())); |  | ||||||
|                                                         }); |  | ||||||
|  |  | ||||||
|                                                         let event_context_menu = { |  | ||||||
|                                                             let event_clone = event.clone(); |  | ||||||
|                                                             let on_event_context_menu = props.on_event_context_menu.clone(); |  | ||||||
|                                                             Callback::from(move |e: MouseEvent| { |  | ||||||
|                                                                 e.prevent_default(); |  | ||||||
|                                                                 e.stop_propagation(); |  | ||||||
|                                                                 if let Some(callback) = &on_event_context_menu { |  | ||||||
|                                                                     callback.emit((e, event_clone.clone())); |  | ||||||
|                                                                 } |  | ||||||
|                                                             }) |  | ||||||
|                                                         }; |  | ||||||
|                                                          |  | ||||||
|                                                         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} |  | ||||||
|                                                                  oncontextmenu={event_context_menu} |  | ||||||
|                                                                  style={format!("background-color: {}", event_color)}> |  | ||||||
|                                                                 { |  | ||||||
|                                                                     if is_refreshing { |  | ||||||
|                                                                         "🔄 Refreshing...".to_string() |  | ||||||
|                                                                     } else if title.len() > 15 { |  | ||||||
|                                                                         format!("{}...", &title[..12]) |  | ||||||
|                                                                     } else { |  | ||||||
|                                                                         title |  | ||||||
|                                                                     } |  | ||||||
|                                                                 } |  | ||||||
|                                                             </div>  |  | ||||||
|                                                         } |  | ||||||
|                                                     }).collect::<Html>() |  | ||||||
|                                                 } |  | ||||||
|                                                 { |  | ||||||
|                                                     if events.len() > 2 { |  | ||||||
|                                                         html! { <div class="more-events">{format!("+{} more", events.len() - 2)}</div> } |  | ||||||
|                                                     } else { |  | ||||||
|                                                         html! {} |  | ||||||
|                                                     } |  | ||||||
|                                                 } |  | ||||||
|                                             </div> |  | ||||||
|                                         } |  | ||||||
|                                     } else { |  | ||||||
|                                         html! {} |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                             </div> |  | ||||||
|                         } |                         } | ||||||
|                     }).collect::<Html>() |                     }, | ||||||
|  |                     ViewMode::Week => html! { | ||||||
|  |                         <WeekView | ||||||
|  |                             current_date={*current_date} | ||||||
|  |                             today={today} | ||||||
|  |                             events={props.events.clone()} | ||||||
|  |                             on_event_click={props.on_event_click.clone()} | ||||||
|  |                             refreshing_event_uid={props.refreshing_event_uid.clone()} | ||||||
|  |                             user_info={props.user_info.clone()} | ||||||
|  |                             on_event_context_menu={props.on_event_context_menu.clone()} | ||||||
|  |                             on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||||
|  |                             on_create_event={Some(on_create_event)} | ||||||
|  |                         /> | ||||||
|  |                     }, | ||||||
|                 } |                 } | ||||||
|                  |             } | ||||||
|                 { render_next_month_days(days_from_prev_month.len(), days_in_month) } |  | ||||||
|             </div> |  | ||||||
|              |              | ||||||
|             // Event details modal |             // Event details modal | ||||||
|             <EventModal  |             <EventModal  | ||||||
| @@ -259,75 +224,47 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|                     }) |                     }) | ||||||
|                 }} |                 }} | ||||||
|             /> |             /> | ||||||
|  |              | ||||||
|  |             // Create event modal | ||||||
|  |             <CreateEventModal | ||||||
|  |                 is_open={*show_create_modal} | ||||||
|  |                 selected_date={create_event_data.as_ref().map(|(date, _, _)| *date)} | ||||||
|  |                 event_to_edit={None} | ||||||
|  |                 available_calendars={props.user_info.as_ref().map(|info| info.calendars.clone()).unwrap_or_default()} | ||||||
|  |                 initial_start_time={create_event_data.as_ref().map(|(_, start_time, _)| *start_time)} | ||||||
|  |                 initial_end_time={create_event_data.as_ref().map(|(_, _, end_time)| *end_time)} | ||||||
|  |                 on_close={{ | ||||||
|  |                     let show_create_modal = show_create_modal.clone(); | ||||||
|  |                     let create_event_data = create_event_data.clone(); | ||||||
|  |                     Callback::from(move |_| { | ||||||
|  |                         show_create_modal.set(false); | ||||||
|  |                         create_event_data.set(None); | ||||||
|  |                     }) | ||||||
|  |                 }} | ||||||
|  |                 on_create={{ | ||||||
|  |                     let show_create_modal = show_create_modal.clone(); | ||||||
|  |                     let create_event_data = create_event_data.clone(); | ||||||
|  |                     let on_create_event_request = props.on_create_event_request.clone(); | ||||||
|  |                     Callback::from(move |event_data: EventCreationData| { | ||||||
|  |                         show_create_modal.set(false); | ||||||
|  |                         create_event_data.set(None); | ||||||
|  |                          | ||||||
|  |                         // Emit the create event request to parent | ||||||
|  |                         if let Some(callback) = &on_create_event_request { | ||||||
|  |                             callback.emit(event_data); | ||||||
|  |                         } | ||||||
|  |                     }) | ||||||
|  |                 }} | ||||||
|  |                 on_update={{ | ||||||
|  |                     let show_create_modal = show_create_modal.clone(); | ||||||
|  |                     let create_event_data = create_event_data.clone(); | ||||||
|  |                     Callback::from(move |(_original_event, _updated_data): (CalendarEvent, EventCreationData)| { | ||||||
|  |                         show_create_modal.set(false); | ||||||
|  |                         create_event_data.set(None); | ||||||
|  |                         // TODO: Handle actual event update | ||||||
|  |                     }) | ||||||
|  |                 }} | ||||||
|  |             /> | ||||||
|         </div> |         </div> | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| fn render_next_month_days(prev_days_count: usize, current_days_count: u32) -> Html { |  | ||||||
|     let total_slots = 42; // 6 rows x 7 days |  | ||||||
|     let used_slots = prev_days_count + current_days_count as usize; |  | ||||||
|     let remaining_slots = if used_slots < total_slots { total_slots - used_slots } else { 0 }; |  | ||||||
|      |  | ||||||
|     (1..=remaining_slots).map(|day| { |  | ||||||
|         html! { |  | ||||||
|             <div class="calendar-day next-month">{day}</div> |  | ||||||
|         } |  | ||||||
|     }).collect::<Html>() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fn get_days_in_month(date: NaiveDate) -> u32 { |  | ||||||
|     NaiveDate::from_ymd_opt( |  | ||||||
|         if date.month() == 12 { date.year() + 1 } else { date.year() }, |  | ||||||
|         if date.month() == 12 { 1 } else { date.month() + 1 }, |  | ||||||
|         1 |  | ||||||
|     ) |  | ||||||
|     .unwrap() |  | ||||||
|     .pred_opt() |  | ||||||
|     .unwrap() |  | ||||||
|     .day() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fn get_days_from_previous_month(current_month: NaiveDate, first_weekday: Weekday) -> Vec<u32> { |  | ||||||
|     let days_before = match first_weekday { |  | ||||||
|         Weekday::Sun => 0, |  | ||||||
|         Weekday::Mon => 1, |  | ||||||
|         Weekday::Tue => 2, |  | ||||||
|         Weekday::Wed => 3, |  | ||||||
|         Weekday::Thu => 4, |  | ||||||
|         Weekday::Fri => 5, |  | ||||||
|         Weekday::Sat => 6, |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     if days_before == 0 { |  | ||||||
|         vec![] |  | ||||||
|     } else { |  | ||||||
|         // Calculate the previous month |  | ||||||
|         let prev_month = if current_month.month() == 1 { |  | ||||||
|             NaiveDate::from_ymd_opt(current_month.year() - 1, 12, 1).unwrap() |  | ||||||
|         } else { |  | ||||||
|             NaiveDate::from_ymd_opt(current_month.year(), current_month.month() - 1, 1).unwrap() |  | ||||||
|         }; |  | ||||||
|          |  | ||||||
|         let prev_month_days = get_days_in_month(prev_month); |  | ||||||
|         ((prev_month_days - days_before as u32 + 1)..=prev_month_days).collect() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fn get_month_name(month: u32) -> &'static str { |  | ||||||
|     match month { |  | ||||||
|         1 => "January", |  | ||||||
|         2 => "February",  |  | ||||||
|         3 => "March", |  | ||||||
|         4 => "April", |  | ||||||
|         5 => "May", |  | ||||||
|         6 => "June", |  | ||||||
|         7 => "July", |  | ||||||
|         8 => "August", |  | ||||||
|         9 => "September", |  | ||||||
|         10 => "October", |  | ||||||
|         11 => "November", |  | ||||||
|         12 => "December", |  | ||||||
|         _ => "Invalid" |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								src/components/calendar_header.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/components/calendar_header.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | use yew::prelude::*; | ||||||
|  | use chrono::{NaiveDate, Datelike}; | ||||||
|  | use crate::components::ViewMode; | ||||||
|  | use web_sys::MouseEvent; | ||||||
|  |  | ||||||
|  | #[derive(Properties, PartialEq)] | ||||||
|  | pub struct CalendarHeaderProps { | ||||||
|  |     pub current_date: NaiveDate, | ||||||
|  |     pub view_mode: ViewMode, | ||||||
|  |     pub on_prev: Callback<MouseEvent>, | ||||||
|  |     pub on_next: Callback<MouseEvent>, | ||||||
|  |     pub on_today: Callback<MouseEvent>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[function_component(CalendarHeader)] | ||||||
|  | pub fn calendar_header(props: &CalendarHeaderProps) -> Html { | ||||||
|  |     let title = format!("{} {}", get_month_name(props.current_date.month()), props.current_date.year()); | ||||||
|  |  | ||||||
|  |     html! { | ||||||
|  |         <div class="calendar-header"> | ||||||
|  |             <button class="nav-button" onclick={props.on_prev.clone()}>{"‹"}</button> | ||||||
|  |             <h2 class="month-year">{title}</h2> | ||||||
|  |             <div class="header-right"> | ||||||
|  |                 <button class="today-button" onclick={props.on_today.clone()}>{"Today"}</button> | ||||||
|  |                 <button class="nav-button" onclick={props.on_next.clone()}>{"›"}</button> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_month_name(month: u32) -> &'static str { | ||||||
|  |     match month { | ||||||
|  |         1 => "January", | ||||||
|  |         2 => "February",  | ||||||
|  |         3 => "March", | ||||||
|  |         4 => "April", | ||||||
|  |         5 => "May", | ||||||
|  |         6 => "June", | ||||||
|  |         7 => "July", | ||||||
|  |         8 => "August", | ||||||
|  |         9 => "September", | ||||||
|  |         10 => "October", | ||||||
|  |         11 => "November", | ||||||
|  |         12 => "December", | ||||||
|  |         _ => "Invalid" | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -12,6 +12,10 @@ pub struct CreateEventModalProps { | |||||||
|     pub on_create: Callback<EventCreationData>, |     pub on_create: Callback<EventCreationData>, | ||||||
|     pub on_update: Callback<(CalendarEvent, EventCreationData)>, // (original_event, updated_data) |     pub on_update: Callback<(CalendarEvent, EventCreationData)>, // (original_event, updated_data) | ||||||
|     pub available_calendars: Vec<CalendarInfo>, |     pub available_calendars: Vec<CalendarInfo>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub initial_start_time: Option<NaiveTime>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub initial_end_time: Option<NaiveTime>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Clone, PartialEq, Debug)] | #[derive(Clone, PartialEq, Debug)] | ||||||
| @@ -159,13 +163,16 @@ impl Default for EventCreationData { | |||||||
| impl EventCreationData { | impl EventCreationData { | ||||||
|     pub fn from_calendar_event(event: &CalendarEvent) -> Self { |     pub fn from_calendar_event(event: &CalendarEvent) -> Self { | ||||||
|         // Convert CalendarEvent to EventCreationData for editing |         // Convert CalendarEvent to EventCreationData for editing | ||||||
|  |         // All events (including temporary drag events) now have proper UTC times | ||||||
|  |         // Convert to local time for display in the modal | ||||||
|  |          | ||||||
|         Self { |         Self { | ||||||
|             title: event.summary.clone().unwrap_or_default(), |             title: event.summary.clone().unwrap_or_default(), | ||||||
|             description: event.description.clone().unwrap_or_default(), |             description: event.description.clone().unwrap_or_default(), | ||||||
|             start_date: event.start.date_naive(), |             start_date: event.start.with_timezone(&chrono::Local).date_naive(), | ||||||
|             start_time: event.start.time(), |             start_time: event.start.with_timezone(&chrono::Local).time(), | ||||||
|             end_date: event.end.as_ref().map(|e| e.date_naive()).unwrap_or(event.start.date_naive()), |             end_date: event.end.as_ref().map(|e| e.with_timezone(&chrono::Local).date_naive()).unwrap_or(event.start.with_timezone(&chrono::Local).date_naive()), | ||||||
|             end_time: event.end.as_ref().map(|e| e.time()).unwrap_or(event.start.time()), |             end_time: event.end.as_ref().map(|e| e.with_timezone(&chrono::Local).time()).unwrap_or(event.start.with_timezone(&chrono::Local).time()), | ||||||
|             location: event.location.clone().unwrap_or_default(), |             location: event.location.clone().unwrap_or_default(), | ||||||
|             all_day: event.all_day, |             all_day: event.all_day, | ||||||
|             status: EventStatus::from_service_status(&event.status), |             status: EventStatus::from_service_status(&event.status), | ||||||
| @@ -187,9 +194,9 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { | |||||||
|     let event_data = use_state(|| EventCreationData::default()); |     let event_data = use_state(|| EventCreationData::default()); | ||||||
|      |      | ||||||
|     // Initialize with selected date or event data if provided |     // Initialize with selected date or event data if provided | ||||||
|     use_effect_with((props.selected_date, props.event_to_edit.clone(), props.is_open, props.available_calendars.clone()), { |     use_effect_with((props.selected_date, props.event_to_edit.clone(), props.is_open, props.available_calendars.clone(), props.initial_start_time, props.initial_end_time), { | ||||||
|         let event_data = event_data.clone(); |         let event_data = event_data.clone(); | ||||||
|         move |(selected_date, event_to_edit, is_open, available_calendars)| { |         move |(selected_date, event_to_edit, is_open, available_calendars, initial_start_time, initial_end_time)| { | ||||||
|             if *is_open { |             if *is_open { | ||||||
|                 let mut data = if let Some(event) = event_to_edit { |                 let mut data = if let Some(event) = event_to_edit { | ||||||
|                     // Pre-populate with event data for editing |                     // Pre-populate with event data for editing | ||||||
| @@ -199,6 +206,15 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { | |||||||
|                     let mut data = EventCreationData::default(); |                     let mut data = EventCreationData::default(); | ||||||
|                     data.start_date = *date; |                     data.start_date = *date; | ||||||
|                     data.end_date = *date; |                     data.end_date = *date; | ||||||
|  |                      | ||||||
|  |                     // Use initial times if provided (from drag-to-create) | ||||||
|  |                     if let Some(start_time) = initial_start_time { | ||||||
|  |                         data.start_time = *start_time; | ||||||
|  |                     } | ||||||
|  |                     if let Some(end_time) = initial_end_time { | ||||||
|  |                         data.end_time = *end_time; | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|                     data |                     data | ||||||
|                 } else { |                 } else { | ||||||
|                     // Default initialization |                     // Default initialization | ||||||
|   | |||||||
| @@ -1,5 +1,8 @@ | |||||||
| pub mod login; | pub mod login; | ||||||
| pub mod calendar; | pub mod calendar; | ||||||
|  | pub mod calendar_header; | ||||||
|  | pub mod month_view; | ||||||
|  | pub mod week_view; | ||||||
| pub mod event_modal; | pub mod event_modal; | ||||||
| pub mod create_calendar_modal; | pub mod create_calendar_modal; | ||||||
| pub mod context_menu; | pub mod context_menu; | ||||||
| @@ -12,12 +15,15 @@ pub mod route_handler; | |||||||
|  |  | ||||||
| pub use login::Login; | pub use login::Login; | ||||||
| pub use calendar::Calendar; | pub use calendar::Calendar; | ||||||
|  | pub use calendar_header::CalendarHeader; | ||||||
|  | pub use month_view::MonthView; | ||||||
|  | pub use week_view::WeekView; | ||||||
| pub use event_modal::EventModal; | pub use event_modal::EventModal; | ||||||
| pub use create_calendar_modal::CreateCalendarModal; | pub use create_calendar_modal::CreateCalendarModal; | ||||||
| pub use context_menu::ContextMenu; | pub use context_menu::ContextMenu; | ||||||
| pub use event_context_menu::{EventContextMenu, DeleteAction}; | pub use event_context_menu::{EventContextMenu, DeleteAction}; | ||||||
| pub use calendar_context_menu::CalendarContextMenu; | pub use calendar_context_menu::CalendarContextMenu; | ||||||
| pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType}; | pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType}; | ||||||
| pub use sidebar::Sidebar; | pub use sidebar::{Sidebar, ViewMode}; | ||||||
| pub use calendar_list_item::CalendarListItem; | pub use calendar_list_item::CalendarListItem; | ||||||
| pub use route_handler::RouteHandler; | pub use route_handler::RouteHandler; | ||||||
							
								
								
									
										203
									
								
								src/components/month_view.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/components/month_view.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | |||||||
|  | use yew::prelude::*; | ||||||
|  | use chrono::{Datelike, NaiveDate, Weekday}; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use web_sys::MouseEvent; | ||||||
|  | use crate::services::calendar_service::{CalendarEvent, UserInfo}; | ||||||
|  |  | ||||||
|  | #[derive(Properties, PartialEq)] | ||||||
|  | pub struct MonthViewProps { | ||||||
|  |     pub current_month: NaiveDate, | ||||||
|  |     pub today: NaiveDate, | ||||||
|  |     pub events: HashMap<NaiveDate, Vec<CalendarEvent>>, | ||||||
|  |     pub on_event_click: Callback<CalendarEvent>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub refreshing_event_uid: Option<String>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub user_info: Option<UserInfo>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub selected_date: Option<NaiveDate>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_day_select: Option<Callback<NaiveDate>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[function_component(MonthView)] | ||||||
|  | pub fn month_view(props: &MonthViewProps) -> Html { | ||||||
|  |     let first_day_of_month = props.current_month.with_day(1).unwrap(); | ||||||
|  |     let days_in_month = get_days_in_month(props.current_month); | ||||||
|  |     let first_weekday = first_day_of_month.weekday(); | ||||||
|  |     let days_from_prev_month = get_days_from_previous_month(props.current_month, first_weekday); | ||||||
|  |  | ||||||
|  |     // 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 { | ||||||
|  |                 if let Some(calendar) = user_info.calendars.iter() | ||||||
|  |                     .find(|cal| &cal.path == calendar_path) { | ||||||
|  |                     return calendar.color.clone(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         "#3B82F6".to_string() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     html! { | ||||||
|  |         <div class="calendar-grid"> | ||||||
|  |             // Weekday headers | ||||||
|  |             <div class="weekday-header">{"Sun"}</div> | ||||||
|  |             <div class="weekday-header">{"Mon"}</div> | ||||||
|  |             <div class="weekday-header">{"Tue"}</div> | ||||||
|  |             <div class="weekday-header">{"Wed"}</div> | ||||||
|  |             <div class="weekday-header">{"Thu"}</div> | ||||||
|  |             <div class="weekday-header">{"Fri"}</div> | ||||||
|  |             <div class="weekday-header">{"Sat"}</div> | ||||||
|  |              | ||||||
|  |             // Days from previous month (grayed out) | ||||||
|  |             { | ||||||
|  |                 days_from_prev_month.iter().map(|day| { | ||||||
|  |                     html! { | ||||||
|  |                         <div class="calendar-day prev-month">{*day}</div> | ||||||
|  |                     } | ||||||
|  |                 }).collect::<Html>() | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // Days of the current month | ||||||
|  |             { | ||||||
|  |                 (1..=days_in_month).map(|day| { | ||||||
|  |                     let date = props.current_month.with_day(day).unwrap(); | ||||||
|  |                     let is_today = date == props.today; | ||||||
|  |                     let is_selected = props.selected_date == Some(date); | ||||||
|  |                     let day_events = props.events.get(&date).cloned().unwrap_or_default(); | ||||||
|  |                      | ||||||
|  |                     html! { | ||||||
|  |                         <div  | ||||||
|  |                             class={classes!( | ||||||
|  |                                 "calendar-day",  | ||||||
|  |                                 if is_today { Some("today") } else { None }, | ||||||
|  |                                 if is_selected { Some("selected") } else { None } | ||||||
|  |                             )} | ||||||
|  |                             onclick={ | ||||||
|  |                                 if let Some(callback) = &props.on_day_select { | ||||||
|  |                                     let callback = callback.clone(); | ||||||
|  |                                     Some(Callback::from(move |e: web_sys::MouseEvent| { | ||||||
|  |                                         e.stop_propagation(); // Prevent other handlers | ||||||
|  |                                         callback.emit(date); | ||||||
|  |                                     })) | ||||||
|  |                                 } else { | ||||||
|  |                                     None | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             oncontextmenu={ | ||||||
|  |                                 if let Some(callback) = &props.on_calendar_context_menu { | ||||||
|  |                                     let callback = callback.clone(); | ||||||
|  |                                     Some(Callback::from(move |e: web_sys::MouseEvent| { | ||||||
|  |                                         e.prevent_default(); | ||||||
|  |                                         callback.emit((e, date)); | ||||||
|  |                                     })) | ||||||
|  |                                 } else { | ||||||
|  |                                     None | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         > | ||||||
|  |                             <div class="day-number">{day}</div> | ||||||
|  |                             <div class="day-events"> | ||||||
|  |                                 { | ||||||
|  |                                     day_events.iter().map(|event| { | ||||||
|  |                                         let event_color = get_event_color(event); | ||||||
|  |                                         let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); | ||||||
|  |                                          | ||||||
|  |                                         let onclick = { | ||||||
|  |                                             let on_event_click = props.on_event_click.clone(); | ||||||
|  |                                             let event = event.clone(); | ||||||
|  |                                             Callback::from(move |_: MouseEvent| { | ||||||
|  |                                                 on_event_click.emit(event.clone()); | ||||||
|  |                                             }) | ||||||
|  |                                         }; | ||||||
|  |                                          | ||||||
|  |                                         let oncontextmenu = { | ||||||
|  |                                             if let Some(callback) = &props.on_event_context_menu { | ||||||
|  |                                                 let callback = callback.clone(); | ||||||
|  |                                                 let event = event.clone(); | ||||||
|  |                                                 Some(Callback::from(move |e: web_sys::MouseEvent| { | ||||||
|  |                                                     e.prevent_default(); | ||||||
|  |                                                     callback.emit((e, event.clone())); | ||||||
|  |                                                 })) | ||||||
|  |                                             } else { | ||||||
|  |                                                 None | ||||||
|  |                                             } | ||||||
|  |                                         }; | ||||||
|  |                                          | ||||||
|  |                                         html! { | ||||||
|  |                                             <div  | ||||||
|  |                                                 class={classes!("event-box", if is_refreshing { Some("refreshing") } else { None })} | ||||||
|  |                                                 style={format!("background-color: {}", event_color)} | ||||||
|  |                                                 {onclick} | ||||||
|  |                                                 {oncontextmenu} | ||||||
|  |                                             > | ||||||
|  |                                                 {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} | ||||||
|  |                                             </div> | ||||||
|  |                                         } | ||||||
|  |                                     }).collect::<Html>() | ||||||
|  |                                 } | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     } | ||||||
|  |                 }).collect::<Html>() | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             { render_next_month_days(days_from_prev_month.len(), days_in_month) } | ||||||
|  |         </div> | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn render_next_month_days(prev_days_count: usize, current_days_count: u32) -> Html { | ||||||
|  |     let total_slots = 42; // 6 rows x 7 days | ||||||
|  |     let used_slots = prev_days_count + current_days_count as usize; | ||||||
|  |     let remaining_slots = if used_slots < total_slots { total_slots - used_slots } else { 0 }; | ||||||
|  |      | ||||||
|  |     (1..=remaining_slots).map(|day| { | ||||||
|  |         html! { | ||||||
|  |             <div class="calendar-day next-month">{day}</div> | ||||||
|  |         } | ||||||
|  |     }).collect::<Html>() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_days_in_month(date: NaiveDate) -> u32 { | ||||||
|  |     NaiveDate::from_ymd_opt( | ||||||
|  |         if date.month() == 12 { date.year() + 1 } else { date.year() }, | ||||||
|  |         if date.month() == 12 { 1 } else { date.month() + 1 }, | ||||||
|  |         1 | ||||||
|  |     ) | ||||||
|  |     .unwrap() | ||||||
|  |     .pred_opt() | ||||||
|  |     .unwrap() | ||||||
|  |     .day() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_days_from_previous_month(current_month: NaiveDate, first_weekday: Weekday) -> Vec<u32> { | ||||||
|  |     let days_before = match first_weekday { | ||||||
|  |         Weekday::Sun => 0, | ||||||
|  |         Weekday::Mon => 1, | ||||||
|  |         Weekday::Tue => 2, | ||||||
|  |         Weekday::Wed => 3, | ||||||
|  |         Weekday::Thu => 4, | ||||||
|  |         Weekday::Fri => 5, | ||||||
|  |         Weekday::Sat => 6, | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     if days_before == 0 { | ||||||
|  |         vec![] | ||||||
|  |     } else { | ||||||
|  |         let prev_month = if current_month.month() == 1 { | ||||||
|  |             NaiveDate::from_ymd_opt(current_month.year() - 1, 12, 1).unwrap() | ||||||
|  |         } else { | ||||||
|  |             NaiveDate::from_ymd_opt(current_month.year(), current_month.month() - 1, 1).unwrap() | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         let prev_month_days = get_days_in_month(prev_month); | ||||||
|  |         ((prev_month_days - days_before as u32 + 1)..=prev_month_days).collect() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| use yew::prelude::*; | use yew::prelude::*; | ||||||
| use yew_router::prelude::*; | use yew_router::prelude::*; | ||||||
| use crate::components::Login; | use crate::components::{Login, ViewMode}; | ||||||
| use crate::services::calendar_service::{UserInfo, CalendarEvent}; | use crate::services::calendar_service::{UserInfo, CalendarEvent}; | ||||||
|  |  | ||||||
| #[derive(Clone, Routable, PartialEq)] | #[derive(Clone, Routable, PartialEq)] | ||||||
| @@ -22,6 +22,10 @@ pub struct RouteHandlerProps { | |||||||
|     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>, |     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>, |     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub view: ViewMode, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[function_component(RouteHandler)] | #[function_component(RouteHandler)] | ||||||
| @@ -31,6 +35,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { | |||||||
|     let on_login = props.on_login.clone(); |     let on_login = props.on_login.clone(); | ||||||
|     let on_event_context_menu = props.on_event_context_menu.clone(); |     let on_event_context_menu = props.on_event_context_menu.clone(); | ||||||
|     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 on_create_event_request = props.on_create_event_request.clone(); | ||||||
|      |      | ||||||
|     html! { |     html! { | ||||||
|         <Switch<Route> render={move |route| { |         <Switch<Route> render={move |route| { | ||||||
| @@ -39,6 +45,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { | |||||||
|             let on_login = on_login.clone(); |             let on_login = on_login.clone(); | ||||||
|             let on_event_context_menu = on_event_context_menu.clone(); |             let on_event_context_menu = on_event_context_menu.clone(); | ||||||
|             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 on_create_event_request = on_create_event_request.clone(); | ||||||
|              |              | ||||||
|             match route { |             match route { | ||||||
|                 Route::Home => { |                 Route::Home => { | ||||||
| @@ -62,6 +70,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html { | |||||||
|                                 user_info={user_info}  |                                 user_info={user_info}  | ||||||
|                                 on_event_context_menu={on_event_context_menu} |                                 on_event_context_menu={on_event_context_menu} | ||||||
|                                 on_calendar_context_menu={on_calendar_context_menu} |                                 on_calendar_context_menu={on_calendar_context_menu} | ||||||
|  |                                 view={view} | ||||||
|  |                                 on_create_event_request={on_create_event_request} | ||||||
|                             />  |                             />  | ||||||
|                         } |                         } | ||||||
|                     } else { |                     } else { | ||||||
| @@ -80,6 +90,10 @@ pub struct CalendarViewProps { | |||||||
|     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>, |     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>, | ||||||
|     #[prop_or_default] |     #[prop_or_default] | ||||||
|     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>, |     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub view: ViewMode, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>, | ||||||
| } | } | ||||||
|  |  | ||||||
| use gloo_storage::{LocalStorage, Storage}; | use gloo_storage::{LocalStorage, Storage}; | ||||||
| @@ -238,6 +252,8 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html { | |||||||
|                                 user_info={props.user_info.clone()} |                                 user_info={props.user_info.clone()} | ||||||
|                                 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()} | ||||||
|  |                                 view={props.view.clone()} | ||||||
|  |                                 on_create_event_request={props.on_create_event_request.clone()} | ||||||
|                             /> |                             /> | ||||||
|                         </div> |                         </div> | ||||||
|                     } |                     } | ||||||
| @@ -250,6 +266,8 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html { | |||||||
|                             user_info={props.user_info.clone()} |                             user_info={props.user_info.clone()} | ||||||
|                             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()} | ||||||
|  |                             view={props.view.clone()} | ||||||
|  |                             on_create_event_request={props.on_create_event_request.clone()} | ||||||
|                         /> |                         /> | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| use yew::prelude::*; | use yew::prelude::*; | ||||||
| use yew_router::prelude::*; | use yew_router::prelude::*; | ||||||
|  | use web_sys::HtmlSelectElement; | ||||||
| use crate::services::calendar_service::UserInfo; | use crate::services::calendar_service::UserInfo; | ||||||
| use crate::components::CalendarListItem; | use crate::components::CalendarListItem; | ||||||
|  |  | ||||||
| @@ -13,6 +14,18 @@ pub enum Route { | |||||||
|     Calendar, |     Calendar, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Clone, PartialEq)] | ||||||
|  | pub enum ViewMode { | ||||||
|  |     Month, | ||||||
|  |     Week, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Default for ViewMode { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         ViewMode::Month | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Properties, PartialEq)] | #[derive(Properties, PartialEq)] | ||||||
| pub struct SidebarProps { | pub struct SidebarProps { | ||||||
|     pub user_info: Option<UserInfo>, |     pub user_info: Option<UserInfo>, | ||||||
| @@ -23,10 +36,27 @@ pub struct SidebarProps { | |||||||
|     pub on_color_picker_toggle: Callback<String>, |     pub on_color_picker_toggle: Callback<String>, | ||||||
|     pub available_colors: Vec<String>, |     pub available_colors: Vec<String>, | ||||||
|     pub on_calendar_context_menu: Callback<(MouseEvent, String)>, |     pub on_calendar_context_menu: Callback<(MouseEvent, String)>, | ||||||
|  |     pub current_view: ViewMode, | ||||||
|  |     pub on_view_change: Callback<ViewMode>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[function_component(Sidebar)] | #[function_component(Sidebar)] | ||||||
| pub fn sidebar(props: &SidebarProps) -> Html { | pub fn sidebar(props: &SidebarProps) -> Html { | ||||||
|  |     let on_view_change = { | ||||||
|  |         let on_view_change = props.on_view_change.clone(); | ||||||
|  |         Callback::from(move |e: Event| { | ||||||
|  |             let target = e.target_dyn_into::<HtmlSelectElement>(); | ||||||
|  |             if let Some(select) = target { | ||||||
|  |                 let value = select.value(); | ||||||
|  |                 let new_view = match value.as_str() { | ||||||
|  |                     "week" => ViewMode::Week, | ||||||
|  |                     _ => ViewMode::Month, | ||||||
|  |                 }; | ||||||
|  |                 on_view_change.emit(new_view); | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     html! { |     html! { | ||||||
|         <aside class="app-sidebar"> |         <aside class="app-sidebar"> | ||||||
|             <div class="sidebar-header"> |             <div class="sidebar-header"> | ||||||
| @@ -82,6 +112,14 @@ pub fn sidebar(props: &SidebarProps) -> Html { | |||||||
|                 <button onclick={props.on_create_calendar.reform(|_| ())} class="create-calendar-button"> |                 <button onclick={props.on_create_calendar.reform(|_| ())} class="create-calendar-button"> | ||||||
|                     {"+ Create Calendar"} |                     {"+ Create Calendar"} | ||||||
|                 </button> |                 </button> | ||||||
|  |                  | ||||||
|  |                 <div class="view-selector"> | ||||||
|  |                     <select class="view-selector-dropdown" onchange={on_view_change}> | ||||||
|  |                         <option value="month" selected={matches!(props.current_view, ViewMode::Month)}>{"Month"}</option> | ||||||
|  |                         <option value="week" selected={matches!(props.current_view, ViewMode::Week)}>{"Week"}</option> | ||||||
|  |                     </select> | ||||||
|  |                 </div> | ||||||
|  |                  | ||||||
|                 <button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button> |                 <button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button> | ||||||
|             </div> |             </div> | ||||||
|         </aside> |         </aside> | ||||||
|   | |||||||
							
								
								
									
										453
									
								
								src/components/week_view.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										453
									
								
								src/components/week_view.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,453 @@ | |||||||
|  | use yew::prelude::*; | ||||||
|  | use chrono::{Datelike, NaiveDate, Duration, Weekday, Local, Timelike, NaiveDateTime, NaiveTime}; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use web_sys::MouseEvent; | ||||||
|  | use crate::services::calendar_service::{CalendarEvent, UserInfo}; | ||||||
|  |  | ||||||
|  | #[derive(Properties, PartialEq)] | ||||||
|  | pub struct WeekViewProps { | ||||||
|  |     pub current_date: NaiveDate, | ||||||
|  |     pub today: NaiveDate, | ||||||
|  |     pub events: HashMap<NaiveDate, Vec<CalendarEvent>>, | ||||||
|  |     pub on_event_click: Callback<CalendarEvent>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub refreshing_event_uid: Option<String>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub user_info: Option<UserInfo>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>, | ||||||
|  |     #[prop_or_default] | ||||||
|  |     pub on_create_event: Option<Callback<(NaiveDate, NaiveDateTime, NaiveDateTime)>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Clone, PartialEq)] | ||||||
|  | struct DragState { | ||||||
|  |     is_dragging: bool, | ||||||
|  |     start_date: NaiveDate, | ||||||
|  |     start_y: f64, | ||||||
|  |     current_y: f64, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[function_component(WeekView)] | ||||||
|  | pub fn week_view(props: &WeekViewProps) -> Html { | ||||||
|  |     let start_of_week = get_start_of_week(props.current_date); | ||||||
|  |     let week_days: Vec<NaiveDate> = (0..7) | ||||||
|  |         .map(|i| start_of_week + Duration::days(i)) | ||||||
|  |         .collect(); | ||||||
|  |  | ||||||
|  |     // Drag state for event creation | ||||||
|  |     let drag_state = use_state(|| None::<DragState>); | ||||||
|  |  | ||||||
|  |     // 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 { | ||||||
|  |                 if let Some(calendar) = user_info.calendars.iter() | ||||||
|  |                     .find(|cal| &cal.path == calendar_path) { | ||||||
|  |                     return calendar.color.clone(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         "#3B82F6".to_string() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Generate time labels - 24 hours plus the final midnight boundary | ||||||
|  |     let mut time_labels: Vec<String> = (0..24).map(|hour| { | ||||||
|  |         if hour == 0 { | ||||||
|  |             "12 AM".to_string() | ||||||
|  |         } else if hour < 12 { | ||||||
|  |             format!("{} AM", hour) | ||||||
|  |         } else if hour == 12 { | ||||||
|  |             "12 PM".to_string() | ||||||
|  |         } else { | ||||||
|  |             format!("{} PM", hour - 12) | ||||||
|  |         } | ||||||
|  |     }).collect(); | ||||||
|  |      | ||||||
|  |     // Add the final midnight boundary to show where the day ends | ||||||
|  |     time_labels.push("12 AM".to_string()); | ||||||
|  |  | ||||||
|  |     html! { | ||||||
|  |         <div class="week-view-container"> | ||||||
|  |             // Header with weekday names and dates | ||||||
|  |             <div class="week-header"> | ||||||
|  |                 <div class="time-gutter"></div> | ||||||
|  |                 { | ||||||
|  |                     week_days.iter().map(|date| { | ||||||
|  |                         let is_today = *date == props.today; | ||||||
|  |                         let weekday_name = get_weekday_name(date.weekday()); | ||||||
|  |                          | ||||||
|  |                         html! { | ||||||
|  |                             <div class={classes!("week-day-header", if is_today { Some("today") } else { None })}> | ||||||
|  |                                 <div class="weekday-name">{weekday_name}</div> | ||||||
|  |                                 <div class="day-number">{date.day()}</div> | ||||||
|  |                             </div> | ||||||
|  |                         } | ||||||
|  |                     }).collect::<Html>() | ||||||
|  |                 } | ||||||
|  |             </div> | ||||||
|  |              | ||||||
|  |             // Scrollable content area with time grid | ||||||
|  |             <div class="week-content"> | ||||||
|  |                 <div class="time-grid"> | ||||||
|  |                     // Time labels | ||||||
|  |                     <div class="time-labels"> | ||||||
|  |                         { | ||||||
|  |                             time_labels.iter().enumerate().map(|(index, time)| { | ||||||
|  |                                 let is_final = index == time_labels.len() - 1; | ||||||
|  |                                 html! { | ||||||
|  |                                     <div class={classes!("time-label", if is_final { Some("final-boundary") } else { None })}> | ||||||
|  |                                         {time} | ||||||
|  |                                     </div> | ||||||
|  |                                 } | ||||||
|  |                             }).collect::<Html>() | ||||||
|  |                         } | ||||||
|  |                     </div> | ||||||
|  |                      | ||||||
|  |                     // Day columns | ||||||
|  |                     <div class="week-days-grid"> | ||||||
|  |                         { | ||||||
|  |                             week_days.iter().enumerate().map(|(_column_index, date)| { | ||||||
|  |                                 let is_today = *date == props.today; | ||||||
|  |                                 let day_events = props.events.get(date).cloned().unwrap_or_default(); | ||||||
|  |                                  | ||||||
|  |                                 // Drag event handlers | ||||||
|  |                                 let drag_state_clone = drag_state.clone(); | ||||||
|  |                                 let date_for_drag = *date; | ||||||
|  |                                  | ||||||
|  |                                 let onmousedown = { | ||||||
|  |                                     let drag_state = drag_state_clone.clone(); | ||||||
|  |                                     Callback::from(move |e: MouseEvent| { | ||||||
|  |                                         // Only handle left-click (button 0) | ||||||
|  |                                         if e.button() != 0 { | ||||||
|  |                                             return; | ||||||
|  |                                         } | ||||||
|  |                                          | ||||||
|  |                                         // Calculate Y position relative to day column container | ||||||
|  |                                         // Use layer_y which gives coordinates relative to positioned ancestor | ||||||
|  |                                         let relative_y = e.layer_y() as f64; | ||||||
|  |                                         let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; | ||||||
|  |                                          | ||||||
|  |                                         // Snap to 15-minute increments | ||||||
|  |                                         let snapped_y = snap_to_15_minutes(relative_y); | ||||||
|  |                                          | ||||||
|  |                                         drag_state.set(Some(DragState { | ||||||
|  |                                             is_dragging: true, | ||||||
|  |                                             start_date: date_for_drag, | ||||||
|  |                                             start_y: snapped_y, | ||||||
|  |                                             current_y: snapped_y, | ||||||
|  |                                         })); | ||||||
|  |                                         e.prevent_default(); | ||||||
|  |                                     }) | ||||||
|  |                                 }; | ||||||
|  |                                  | ||||||
|  |                                 let onmousemove = { | ||||||
|  |                                     let drag_state = drag_state_clone.clone(); | ||||||
|  |                                     Callback::from(move |e: MouseEvent| { | ||||||
|  |                                         if let Some(mut current_drag) = (*drag_state).clone() { | ||||||
|  |                                             if current_drag.is_dragging { | ||||||
|  |                                                 // Use layer_y for consistent coordinate calculation | ||||||
|  |                                                 let relative_y = e.layer_y() as f64; | ||||||
|  |                                                 let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; | ||||||
|  |                                                  | ||||||
|  |                                                 // Snap to 15-minute increments | ||||||
|  |                                                 let snapped_y = snap_to_15_minutes(relative_y); | ||||||
|  |                                                  | ||||||
|  |                                                 current_drag.current_y = snapped_y; | ||||||
|  |                                                 drag_state.set(Some(current_drag)); | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|  |                                     }) | ||||||
|  |                                 }; | ||||||
|  |                                  | ||||||
|  |                                 let onmouseup = { | ||||||
|  |                                     let drag_state = drag_state_clone.clone(); | ||||||
|  |                                     let on_create_event = props.on_create_event.clone(); | ||||||
|  |                                     Callback::from(move |_e: MouseEvent| { | ||||||
|  |                                         if let Some(current_drag) = (*drag_state).clone() { | ||||||
|  |                                             if current_drag.is_dragging { | ||||||
|  |                                                 // Calculate start and end times | ||||||
|  |                                                 let start_time = pixels_to_time(current_drag.start_y); | ||||||
|  |                                                 let end_time = pixels_to_time(current_drag.current_y); | ||||||
|  |                                                  | ||||||
|  |                                                 // Ensure start is before end | ||||||
|  |                                                 let (actual_start, actual_end) = if start_time <= end_time { | ||||||
|  |                                                     (start_time, end_time) | ||||||
|  |                                                 } else { | ||||||
|  |                                                     (end_time, start_time) | ||||||
|  |                                                 }; | ||||||
|  |                                                  | ||||||
|  |                                                 // Ensure minimum duration (15 minutes) | ||||||
|  |                                                 let actual_end = if actual_end.signed_duration_since(actual_start).num_minutes() < 15 { | ||||||
|  |                                                     actual_start + chrono::Duration::minutes(15) | ||||||
|  |                                                 } else { | ||||||
|  |                                                     actual_end | ||||||
|  |                                                 }; | ||||||
|  |                                                  | ||||||
|  |                                                 let start_datetime = NaiveDateTime::new(current_drag.start_date, actual_start); | ||||||
|  |                                                 let end_datetime = NaiveDateTime::new(current_drag.start_date, actual_end); | ||||||
|  |                                                  | ||||||
|  |                                                 if let Some(callback) = &on_create_event { | ||||||
|  |                                                     callback.emit((current_drag.start_date, start_datetime, end_datetime)); | ||||||
|  |                                                 } | ||||||
|  |                                                  | ||||||
|  |                                                 drag_state.set(None); | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|  |                                     }) | ||||||
|  |                                 }; | ||||||
|  |                                  | ||||||
|  |                                 html! { | ||||||
|  |                                     <div  | ||||||
|  |                                         class={classes!("week-day-column", if is_today { Some("today") } else { None })} | ||||||
|  |                                         {onmousedown} | ||||||
|  |                                         {onmousemove} | ||||||
|  |                                         {onmouseup} | ||||||
|  |                                     > | ||||||
|  |                                         // Time slot backgrounds - 24 full hour slots + 1 boundary slot | ||||||
|  |                                         { | ||||||
|  |                                             (0..24).map(|_hour| { | ||||||
|  |                                                 html! { | ||||||
|  |                                                     <div class="time-slot"> | ||||||
|  |                                                         <div class="time-slot-half"></div> | ||||||
|  |                                                         <div class="time-slot-half"></div> | ||||||
|  |                                                     </div> | ||||||
|  |                                                 } | ||||||
|  |                                             }).collect::<Html>() | ||||||
|  |                                         } | ||||||
|  |                                         // Final boundary slot to match the final time label | ||||||
|  |                                         <div class="time-slot boundary-slot"></div> | ||||||
|  |                                          | ||||||
|  |                                         // Events positioned absolutely based on their actual times | ||||||
|  |                                         <div class="events-container"> | ||||||
|  |                                             { | ||||||
|  |                                                 day_events.iter().filter_map(|event| { | ||||||
|  |                                                     let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date); | ||||||
|  |                                                      | ||||||
|  |                                                     // Skip events that don't belong on this date or have invalid positioning | ||||||
|  |                                                     if start_pixels == 0.0 && duration_pixels == 0.0 && !is_all_day { | ||||||
|  |                                                         return None; | ||||||
|  |                                                     } | ||||||
|  |                                                      | ||||||
|  |                                                     let event_color = get_event_color(event); | ||||||
|  |                                                     let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); | ||||||
|  |                                                      | ||||||
|  |                                                     let onclick = { | ||||||
|  |                                                         let on_event_click = props.on_event_click.clone(); | ||||||
|  |                                                         let event = event.clone(); | ||||||
|  |                                                         Callback::from(move |e: MouseEvent| { | ||||||
|  |                                                             e.stop_propagation(); // Prevent calendar click events from also triggering | ||||||
|  |                                                             on_event_click.emit(event.clone()); | ||||||
|  |                                                         }) | ||||||
|  |                                                     }; | ||||||
|  |                                                      | ||||||
|  |                                                     let onmousedown_event = { | ||||||
|  |                                                         Callback::from(move |e: MouseEvent| { | ||||||
|  |                                                             e.stop_propagation(); // Prevent drag-to-create from starting on event clicks | ||||||
|  |                                                         }) | ||||||
|  |                                                     }; | ||||||
|  |                                                      | ||||||
|  |                                                     let oncontextmenu = { | ||||||
|  |                                                         if let Some(callback) = &props.on_event_context_menu { | ||||||
|  |                                                             let callback = callback.clone(); | ||||||
|  |                                                             let event = event.clone(); | ||||||
|  |                                                             Some(Callback::from(move |e: web_sys::MouseEvent| { | ||||||
|  |                                                                 e.prevent_default(); | ||||||
|  |                                                                 e.stop_propagation(); // Prevent calendar context menu from also triggering | ||||||
|  |                                                                 callback.emit((e, event.clone())); | ||||||
|  |                                                             })) | ||||||
|  |                                                         } else { | ||||||
|  |                                                             None | ||||||
|  |                                                         } | ||||||
|  |                                                     }; | ||||||
|  |                                                      | ||||||
|  |                                                     // Format time display for the event | ||||||
|  |                                                     let time_display = if event.all_day { | ||||||
|  |                                                         "All Day".to_string() | ||||||
|  |                                                     } else { | ||||||
|  |                                                         let local_start = event.start.with_timezone(&Local); | ||||||
|  |                                                         if let Some(end) = event.end { | ||||||
|  |                                                             let local_end = end.with_timezone(&Local); | ||||||
|  |                                                              | ||||||
|  |                                                             // Check if both times are in same AM/PM period to avoid redundancy | ||||||
|  |                                                             let start_is_am = local_start.hour() < 12; | ||||||
|  |                                                             let end_is_am = local_end.hour() < 12; | ||||||
|  |                                                              | ||||||
|  |                                                             if start_is_am == end_is_am { | ||||||
|  |                                                                 // Same AM/PM period - show "9:00 - 10:30 AM" | ||||||
|  |                                                                 format!("{} - {}",  | ||||||
|  |                                                                     local_start.format("%I:%M").to_string().trim_start_matches('0'), | ||||||
|  |                                                                     local_end.format("%I:%M %p") | ||||||
|  |                                                                 ) | ||||||
|  |                                                             } else { | ||||||
|  |                                                                 // Different AM/PM periods - show "9:00 AM - 2:30 PM" | ||||||
|  |                                                                 format!("{} - {}",  | ||||||
|  |                                                                     local_start.format("%I:%M %p"), | ||||||
|  |                                                                     local_end.format("%I:%M %p") | ||||||
|  |                                                                 ) | ||||||
|  |                                                             } | ||||||
|  |                                                         } else { | ||||||
|  |                                                             // No end time, just show start time | ||||||
|  |                                                             format!("{}", local_start.format("%I:%M %p")) | ||||||
|  |                                                         } | ||||||
|  |                                                     }; | ||||||
|  |                                                      | ||||||
|  |                                                     Some(html! { | ||||||
|  |                                                         <div  | ||||||
|  |                                                             class={classes!( | ||||||
|  |                                                                 "week-event",  | ||||||
|  |                                                                 if is_refreshing { Some("refreshing") } else { None }, | ||||||
|  |                                                                 if is_all_day { Some("all-day") } else { None } | ||||||
|  |                                                             )} | ||||||
|  |                                                             style={format!( | ||||||
|  |                                                                 "background-color: {}; top: {}px; height: {}px;",  | ||||||
|  |                                                                 event_color,  | ||||||
|  |                                                                 start_pixels,  | ||||||
|  |                                                                 duration_pixels | ||||||
|  |                                                             )} | ||||||
|  |                                                             {onclick} | ||||||
|  |                                                             {oncontextmenu} | ||||||
|  |                                                             onmousedown={onmousedown_event} | ||||||
|  |                                                         > | ||||||
|  |                                                             <div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div> | ||||||
|  |                                                             {if !is_all_day { | ||||||
|  |                                                                 html! { <div class="event-time">{time_display}</div> } | ||||||
|  |                                                             } else { | ||||||
|  |                                                                 html! {} | ||||||
|  |                                                             }} | ||||||
|  |                                                         </div> | ||||||
|  |                                                     }) | ||||||
|  |                                                 }).collect::<Html>() | ||||||
|  |                                             } | ||||||
|  |                                         </div> | ||||||
|  |                                          | ||||||
|  |                                         // Temporary event box during drag | ||||||
|  |                                         { | ||||||
|  |                                             if let Some(drag) = (*drag_state).clone() { | ||||||
|  |                                                 if drag.is_dragging && drag.start_date == *date { | ||||||
|  |                                                     let start_y = drag.start_y.min(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); | ||||||
|  |                                                      | ||||||
|  |                                                     // Convert pixels to times for display | ||||||
|  |                                                     let start_time = pixels_to_time(start_y); | ||||||
|  |                                                     let end_time = pixels_to_time(end_y); | ||||||
|  |                                                      | ||||||
|  |                                                     html! { | ||||||
|  |                                                         <div | ||||||
|  |                                                             class="temp-event-box" | ||||||
|  |                                                             style={format!("top: {}px; height: {}px;", start_y, height)} | ||||||
|  |                                                         > | ||||||
|  |                                                             {format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))} | ||||||
|  |                                                         </div> | ||||||
|  |                                                     } | ||||||
|  |                                                 } else { | ||||||
|  |                                                     html! {} | ||||||
|  |                                                 } | ||||||
|  |                                             } else { | ||||||
|  |                                                 html! {} | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|  |                                     </div> | ||||||
|  |                                 } | ||||||
|  |                             }).collect::<Html>() | ||||||
|  |                         } | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_start_of_week(date: NaiveDate) -> NaiveDate { | ||||||
|  |     let weekday = date.weekday(); | ||||||
|  |     let days_from_sunday = match weekday { | ||||||
|  |         Weekday::Sun => 0, | ||||||
|  |         Weekday::Mon => 1, | ||||||
|  |         Weekday::Tue => 2, | ||||||
|  |         Weekday::Wed => 3, | ||||||
|  |         Weekday::Thu => 4, | ||||||
|  |         Weekday::Fri => 5, | ||||||
|  |         Weekday::Sat => 6, | ||||||
|  |     }; | ||||||
|  |     date - Duration::days(days_from_sunday) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fn get_weekday_name(weekday: Weekday) -> &'static str { | ||||||
|  |     match weekday { | ||||||
|  |         Weekday::Sun => "Sun", | ||||||
|  |         Weekday::Mon => "Mon", | ||||||
|  |         Weekday::Tue => "Tue", | ||||||
|  |         Weekday::Wed => "Wed", | ||||||
|  |         Weekday::Thu => "Thu", | ||||||
|  |         Weekday::Fri => "Fri", | ||||||
|  |         Weekday::Sat => "Sat", | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Calculate the pixel position of an event based on its time | ||||||
|  | // Each hour is 60px, so we convert time to pixels | ||||||
|  | // Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes) | ||||||
|  | fn snap_to_15_minutes(pixels: f64) -> f64 { | ||||||
|  |     let increment = 15.0; // 15px = 15 minutes | ||||||
|  |     (pixels / increment).round() * increment | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Convert pixel position to time (inverse of time to pixels) | ||||||
|  | fn pixels_to_time(pixels: f64) -> NaiveTime { | ||||||
|  |     // Since 60px = 1 hour, pixels directly represent minutes | ||||||
|  |     let total_minutes = pixels; // 1px = 1 minute | ||||||
|  |     let hours = (total_minutes / 60.0) as u32; | ||||||
|  |     let minutes = (total_minutes % 60.0) as u32; | ||||||
|  |      | ||||||
|  |     // Clamp to valid time range | ||||||
|  |     let hours = hours.min(23); | ||||||
|  |     let minutes = minutes.min(59); | ||||||
|  |      | ||||||
|  |     NaiveTime::from_hms_opt(hours, minutes, 0).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | fn calculate_event_position(event: &CalendarEvent, date: NaiveDate) -> (f32, f32, bool) { | ||||||
|  |     // Convert UTC times to local time for display | ||||||
|  |     let local_start = event.start.with_timezone(&Local); | ||||||
|  |     let event_date = local_start.date_naive(); | ||||||
|  |      | ||||||
|  |     // Only position events that are on this specific date | ||||||
|  |     if event_date != date { | ||||||
|  |         return (0.0, 0.0, false); // Event not on this date | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Handle all-day events - they appear at the top | ||||||
|  |     if event.all_day { | ||||||
|  |         return (0.0, 30.0, true); // Position at top, 30px height, is_all_day = true | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Calculate start position in pixels from midnight | ||||||
|  |     let start_hour = local_start.hour() as f32; | ||||||
|  |     let start_minute = local_start.minute() as f32; | ||||||
|  |     let start_pixels = (start_hour + start_minute / 60.0) * 60.0; // 60px per hour | ||||||
|  |      | ||||||
|  |     // Calculate duration and height | ||||||
|  |     let duration_pixels = if let Some(end) = event.end { | ||||||
|  |         let local_end = end.with_timezone(&Local); | ||||||
|  |         let end_date = local_end.date_naive(); | ||||||
|  |          | ||||||
|  |         // Handle events that span multiple days by capping at midnight | ||||||
|  |         if end_date > date { | ||||||
|  |             // Event continues past midnight, cap at 24:00 (1440px) | ||||||
|  |             1440.0 - start_pixels | ||||||
|  |         } else { | ||||||
|  |             let end_hour = local_end.hour() as f32; | ||||||
|  |             let end_minute = local_end.minute() as f32; | ||||||
|  |             let end_pixels = (end_hour + end_minute / 60.0) * 60.0; | ||||||
|  |             (end_pixels - start_pixels).max(20.0) // Minimum 20px height | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         60.0 // Default 1 hour if no end time | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     (start_pixels, duration_pixels, false) // is_all_day = false | ||||||
|  | } | ||||||
							
								
								
									
										286
									
								
								styles.css
									
									
									
									
									
								
							
							
						
						
									
										286
									
								
								styles.css
									
									
									
									
									
								
							| @@ -462,6 +462,246 @@ body { | |||||||
|     background: white; |     background: white; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* Week View Container */ | ||||||
|  | .week-view-container { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     height: 100%; | ||||||
|  |     background: white; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Week Header */ | ||||||
|  | .week-header { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 80px repeat(7, 1fr); | ||||||
|  |     background: #f8f9fa; | ||||||
|  |     border-bottom: 2px solid #e9ecef; | ||||||
|  |     position: sticky; | ||||||
|  |     top: 0; | ||||||
|  |     z-index: 10; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-gutter { | ||||||
|  |     background: #f8f9fa; | ||||||
|  |     border-right: 1px solid #e9ecef; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-header { | ||||||
|  |     padding: 1rem; | ||||||
|  |     text-align: center; | ||||||
|  |     border-right: 1px solid #e9ecef; | ||||||
|  |     background: #f8f9fa; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-header.today { | ||||||
|  |     background: #e3f2fd; | ||||||
|  |     color: #1976d2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .weekday-name { | ||||||
|  |     font-size: 0.9rem; | ||||||
|  |     font-weight: 600; | ||||||
|  |     color: #666; | ||||||
|  |     text-transform: uppercase; | ||||||
|  |     letter-spacing: 0.5px; | ||||||
|  |     margin-bottom: 0.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-header .day-number { | ||||||
|  |     font-size: 1.5rem; | ||||||
|  |     font-weight: 700; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-header.today .weekday-name { | ||||||
|  |     color: #1976d2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Week Content */ | ||||||
|  | .week-content { | ||||||
|  |     flex: 1; | ||||||
|  |     overflow-y: auto; | ||||||
|  |     overflow-x: hidden; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-grid { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 80px 1fr; | ||||||
|  |     min-height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Time Labels */ | ||||||
|  | .time-labels { | ||||||
|  |     background: #f8f9fa; | ||||||
|  |     border-right: 1px solid #e9ecef; | ||||||
|  |     position: sticky; | ||||||
|  |     left: 0; | ||||||
|  |     z-index: 5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-label { | ||||||
|  |     height: 60px; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: flex-start; | ||||||
|  |     justify-content: center; | ||||||
|  |     padding-top: 0.5rem; | ||||||
|  |     font-size: 0.75rem; | ||||||
|  |     color: #666; | ||||||
|  |     border-bottom: 1px solid #f0f0f0; | ||||||
|  |     font-weight: 500; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-label.final-boundary { | ||||||
|  |     height: 60px; /* Keep same height but this marks the end boundary */ | ||||||
|  |     border-bottom: 2px solid #e9ecef; /* Stronger border to show day end */ | ||||||
|  |     color: #999; /* Lighter color to indicate it's the boundary */ | ||||||
|  |     font-size: 0.7rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Week Days Grid */ | ||||||
|  | .week-days-grid { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: repeat(7, 1fr); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-column { | ||||||
|  |     position: relative; | ||||||
|  |     border-right: 1px solid #e9ecef; | ||||||
|  |     min-height: 1500px; /* 25 time labels × 60px = 1500px total */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-column:last-child { | ||||||
|  |     border-right: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-day-column.today { | ||||||
|  |     background: #fafffe; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Time Slots */ | ||||||
|  | .time-slot { | ||||||
|  |     height: 60px; | ||||||
|  |     border-bottom: 1px solid #f0f0f0; | ||||||
|  |     position: relative; | ||||||
|  |     pointer-events: none; /* Don't capture mouse events */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-slot-half { | ||||||
|  |     height: 30px; | ||||||
|  |     border-bottom: 1px dotted #f5f5f5; | ||||||
|  |     pointer-events: none; /* Don't capture mouse events */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-slot-half:last-child { | ||||||
|  |     border-bottom: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .time-slot.boundary-slot { | ||||||
|  |     height: 60px; /* Match the final time label height */ | ||||||
|  |     border-bottom: 2px solid #e9ecef; /* Strong border to match final boundary */ | ||||||
|  |     background: rgba(0,0,0,0.02); /* Slightly different background to indicate boundary */ | ||||||
|  |     pointer-events: none; /* Don't capture mouse events */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Events Container */ | ||||||
|  | .events-container { | ||||||
|  |     position: absolute; | ||||||
|  |     top: 0; | ||||||
|  |     left: 0; | ||||||
|  |     right: 0; | ||||||
|  |     bottom: 0; | ||||||
|  |     pointer-events: none; /* Container doesn't capture, but children (events) do */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Week Events */ | ||||||
|  | .week-event { | ||||||
|  |     position: absolute; | ||||||
|  |     left: 4px; | ||||||
|  |     right: 4px; | ||||||
|  |     min-height: 20px; | ||||||
|  |     background: #3B82F6; | ||||||
|  |     color: white; | ||||||
|  |     padding: 2px 6px; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     font-size: 0.75rem; | ||||||
|  |     line-height: 1.3; | ||||||
|  |     cursor: pointer; | ||||||
|  |     pointer-events: auto; | ||||||
|  |     z-index: 3; | ||||||
|  |     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 3px rgba(0,0,0,0.1); | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |     white-space: nowrap; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-event:hover { | ||||||
|  |     filter: brightness(1.1); | ||||||
|  |     z-index: 4; | ||||||
|  |     box-shadow: 0 2px 6px rgba(0,0,0,0.15); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-event.refreshing { | ||||||
|  |     animation: pulse 1.5s ease-in-out infinite alternate; | ||||||
|  |     border-color: #ff9800; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Temporary event box during drag creation */ | ||||||
|  | .temp-event-box { | ||||||
|  |     position: absolute; | ||||||
|  |     left: 4px; | ||||||
|  |     right: 4px; | ||||||
|  |     background: rgba(59, 130, 246, 0.3); | ||||||
|  |     border: 2px dashed rgba(59, 130, 246, 0.8); | ||||||
|  |     border-radius: 4px; | ||||||
|  |     color: rgba(59, 130, 246, 0.9); | ||||||
|  |     font-size: 0.75rem; | ||||||
|  |     font-weight: 600; | ||||||
|  |     padding: 4px 6px; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     pointer-events: none; | ||||||
|  |     z-index: 6; /* Higher than events */ | ||||||
|  |     text-align: center; | ||||||
|  |     user-select: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-event .event-title { | ||||||
|  |     font-weight: 600; | ||||||
|  |     margin-bottom: 2px; | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |     white-space: nowrap; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-event .event-time { | ||||||
|  |     font-size: 0.65rem; | ||||||
|  |     opacity: 0.9; | ||||||
|  |     font-weight: 400; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-event.all-day { | ||||||
|  |     opacity: 0.9; | ||||||
|  |     border-left: 4px solid rgba(255,255,255,0.5); | ||||||
|  |     font-style: italic; | ||||||
|  |     background: linear-gradient(135deg, var(--event-color, #3B82F6), rgba(255,255,255,0.1)) !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Legacy Week Grid (for backward compatibility) */ | ||||||
|  | .week-grid { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: repeat(7, 1fr); | ||||||
|  |     grid-template-rows: auto 1fr; | ||||||
|  |     flex: 1; | ||||||
|  |     background: white; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .week-view .calendar-day { | ||||||
|  |     height: 100%; /* Make week view days stretch to full height of their grid cell */ | ||||||
|  | } | ||||||
|  |  | ||||||
| .weekday-header { | .weekday-header { | ||||||
|     background: #f8f9fa; |     background: #f8f9fa; | ||||||
|     padding: 1rem; |     padding: 1rem; | ||||||
| @@ -773,6 +1013,15 @@ body { | |||||||
|         border-top: none; |         border-top: none; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     .view-selector { | ||||||
|  |         margin-bottom: 0.5rem; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .view-selector-dropdown { | ||||||
|  |         padding: 0.5rem 0.75rem; | ||||||
|  |         font-size: 0.8rem; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     .app-main { |     .app-main { | ||||||
|         margin-left: 0; |         margin-left: 0; | ||||||
|         max-width: 100%; |         max-width: 100%; | ||||||
| @@ -889,6 +1138,43 @@ body { | |||||||
|     transform: translateY(0); |     transform: translateY(0); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* View Selector */ | ||||||
|  | .view-selector { | ||||||
|  |     margin-bottom: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .view-selector-dropdown { | ||||||
|  |     width: 100%; | ||||||
|  |     background: rgba(255, 255, 255, 0.1); | ||||||
|  |     border: 1px solid rgba(255, 255, 255, 0.2); | ||||||
|  |     color: white; | ||||||
|  |     padding: 0.75rem 1rem; | ||||||
|  |     border-radius: 8px; | ||||||
|  |     font-size: 0.9rem; | ||||||
|  |     font-weight: 500; | ||||||
|  |     cursor: pointer; | ||||||
|  |     transition: all 0.2s ease; | ||||||
|  |     backdrop-filter: blur(10px); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .view-selector-dropdown:hover { | ||||||
|  |     background: rgba(255, 255, 255, 0.2); | ||||||
|  |     border-color: rgba(255, 255, 255, 0.3); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .view-selector-dropdown:focus { | ||||||
|  |     outline: none; | ||||||
|  |     background: rgba(255, 255, 255, 0.2); | ||||||
|  |     border-color: rgba(255, 255, 255, 0.4); | ||||||
|  |     box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .view-selector-dropdown option { | ||||||
|  |     background: #2a2a2a; | ||||||
|  |     color: white; | ||||||
|  |     padding: 0.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
| /* Create Calendar Modal */ | /* Create Calendar Modal */ | ||||||
| .modal-backdrop { | .modal-backdrop { | ||||||
|     position: fixed; |     position: fixed; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user