use yew::prelude::*; use yew_router::prelude::*; use gloo_storage::{LocalStorage, Storage}; use web_sys::MouseEvent; use crate::components::{Sidebar, ViewMode, Theme, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction}; use crate::services::{CalendarService, calendar_service::UserInfo}; use crate::models::ical::VEvent; use chrono::NaiveDate; fn get_theme_event_colors() -> Vec { if let Some(window) = web_sys::window() { if let Some(document) = window.document() { if let Some(root) = document.document_element() { if let Ok(Some(computed_style)) = window.get_computed_style(&root) { if let Ok(colors_string) = computed_style.get_property_value("--event-colors") { if !colors_string.is_empty() { return colors_string .split(',') .map(|color| color.trim().to_string()) .filter(|color| !color.is_empty()) .collect(); } } } } } } vec![ "#3B82F6".to_string(), "#10B981".to_string(), "#F59E0B".to_string(), "#EF4444".to_string(), "#8B5CF6".to_string(), "#06B6D4".to_string(), "#84CC16".to_string(), "#F97316".to_string(), "#EC4899".to_string(), "#6366F1".to_string(), "#14B8A6".to_string(), "#F3B806".to_string(), "#8B5A2B".to_string(), "#6B7280".to_string(), "#DC2626".to_string(), "#7C3AED".to_string() ] } #[function_component] pub fn App() -> Html { let auth_token = use_state(|| -> Option { LocalStorage::get("auth_token").ok() }); let user_info = use_state(|| -> Option { None }); let color_picker_open = use_state(|| -> Option { None }); let create_modal_open = use_state(|| false); let context_menu_open = use_state(|| false); let context_menu_pos = use_state(|| (0i32, 0i32)); let context_menu_calendar_path = use_state(|| -> Option { None }); let event_context_menu_open = use_state(|| false); let event_context_menu_pos = use_state(|| (0i32, 0i32)); let event_context_menu_event = use_state(|| -> Option { None }); let calendar_context_menu_open = use_state(|| false); let calendar_context_menu_pos = use_state(|| (0i32, 0i32)); let calendar_context_menu_date = use_state(|| -> Option { None }); let create_event_modal_open = use_state(|| false); let selected_date_for_event = use_state(|| -> Option { 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::("calendar_view_mode") { match saved_view.as_str() { "week" => ViewMode::Week, _ => ViewMode::Month, } } else { ViewMode::Month // Default to month view } }); // Theme state - load from localStorage if available let current_theme = use_state(|| { // Try to load saved theme from localStorage if let Ok(saved_theme) = LocalStorage::get::("calendar_theme") { Theme::from_value(&saved_theme) } else { Theme::Default // Default theme } }); let available_colors = use_state(|| get_theme_event_colors()); let on_login = { let auth_token = auth_token.clone(); Callback::from(move |token: String| { auth_token.set(Some(token)); }) }; let on_logout = { let auth_token = auth_token.clone(); let user_info = user_info.clone(); Callback::from(move |_| { let _ = LocalStorage::delete("auth_token"); auth_token.set(None); user_info.set(None); }) }; 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); }) }; let on_theme_change = { let current_theme = current_theme.clone(); let available_colors = available_colors.clone(); Callback::from(move |new_theme: Theme| { // Save theme to localStorage let _ = LocalStorage::set("calendar_theme", new_theme.value()); // Apply theme to document root if let Some(document) = web_sys::window().and_then(|w| w.document()) { if let Some(root) = document.document_element() { let _ = root.set_attribute("data-theme", new_theme.value()); } } // Update state current_theme.set(new_theme); // Update available colors after theme change available_colors.set(get_theme_event_colors()); }) }; // Apply initial theme on mount { let current_theme = current_theme.clone(); use_effect_with((), move |_| { let theme = (*current_theme).clone(); if let Some(document) = web_sys::window().and_then(|w| w.document()) { if let Some(root) = document.document_element() { let _ = root.set_attribute("data-theme", theme.value()); } } }); } // Fetch user info when token is available { let user_info = user_info.clone(); let auth_token = auth_token.clone(); use_effect_with((*auth_token).clone(), move |token| { if let Some(token) = token { let user_info = user_info.clone(); let token = token.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; if !password.is_empty() { match calendar_service.fetch_user_info(&token, &password).await { Ok(mut info) => { if let Ok(saved_colors_json) = LocalStorage::get::("calendar_colors") { if let Ok(saved_info) = serde_json::from_str::(&saved_colors_json) { for saved_cal in &saved_info.calendars { for cal in &mut info.calendars { if cal.path == saved_cal.path { cal.color = saved_cal.color.clone(); } } } } } user_info.set(Some(info)); } Err(err) => { web_sys::console::log_1(&format!("Failed to fetch user info: {}", err).into()); } } } }); } else { user_info.set(None); } || () }); } let on_outside_click = { let color_picker_open = color_picker_open.clone(); let context_menu_open = context_menu_open.clone(); let event_context_menu_open = event_context_menu_open.clone(); let calendar_context_menu_open = calendar_context_menu_open.clone(); Callback::from(move |e: MouseEvent| { // Check if any context menu or color picker is open let any_menu_open = color_picker_open.is_some() || *context_menu_open || *event_context_menu_open || *calendar_context_menu_open; if any_menu_open { // Prevent the default action and stop event propagation e.prevent_default(); e.stop_propagation(); } // Close all open menus/pickers color_picker_open.set(None); context_menu_open.set(false); event_context_menu_open.set(false); calendar_context_menu_open.set(false); }) }; // Compute if any context menu is open let any_context_menu_open = color_picker_open.is_some() || *context_menu_open || *event_context_menu_open || *calendar_context_menu_open; let on_color_change = { let user_info = user_info.clone(); let color_picker_open = color_picker_open.clone(); Callback::from(move |(calendar_path, color): (String, String)| { if let Some(mut info) = (*user_info).clone() { for calendar in &mut info.calendars { if calendar.path == calendar_path { calendar.color = color.clone(); break; } } user_info.set(Some(info.clone())); if let Ok(json) = serde_json::to_string(&info) { let _ = LocalStorage::set("calendar_colors", json); } } color_picker_open.set(None); }) }; let on_color_picker_toggle = { let color_picker_open = color_picker_open.clone(); Callback::from(move |calendar_path: String| { if color_picker_open.as_ref() == Some(&calendar_path) { color_picker_open.set(None); } else { color_picker_open.set(Some(calendar_path)); } }) }; let on_calendar_context_menu = { let context_menu_open = context_menu_open.clone(); let context_menu_pos = context_menu_pos.clone(); let context_menu_calendar_path = context_menu_calendar_path.clone(); Callback::from(move |(event, calendar_path): (MouseEvent, String)| { context_menu_open.set(true); context_menu_pos.set((event.client_x(), event.client_y())); context_menu_calendar_path.set(Some(calendar_path)); }) }; let on_event_context_menu = { let event_context_menu_open = event_context_menu_open.clone(); let event_context_menu_pos = event_context_menu_pos.clone(); let event_context_menu_event = event_context_menu_event.clone(); Callback::from(move |(event, calendar_event): (MouseEvent, VEvent)| { event_context_menu_open.set(true); event_context_menu_pos.set((event.client_x(), event.client_y())); event_context_menu_event.set(Some(calendar_event)); }) }; let on_calendar_date_context_menu = { let calendar_context_menu_open = calendar_context_menu_open.clone(); let calendar_context_menu_pos = calendar_context_menu_pos.clone(); let calendar_context_menu_date = calendar_context_menu_date.clone(); Callback::from(move |(event, date): (MouseEvent, NaiveDate)| { calendar_context_menu_open.set(true); calendar_context_menu_pos.set((event.client_x(), event.client_y())); calendar_context_menu_date.set(Some(date)); }) }; let on_create_event_click = { let create_event_modal_open = create_event_modal_open.clone(); let selected_date_for_event = selected_date_for_event.clone(); let calendar_context_menu_date = calendar_context_menu_date.clone(); Callback::from(move |_: MouseEvent| { create_event_modal_open.set(true); selected_date_for_event.set((*calendar_context_menu_date).clone()); }) }; let on_event_create = { let create_event_modal_open = create_event_modal_open.clone(); let auth_token = auth_token.clone(); Callback::from(move |event_data: EventCreationData| { web_sys::console::log_1(&format!("Creating event: {:?}", event_data).into()); create_event_modal_open.set(false); if let Some(_token) = (*auth_token).clone() { wasm_bindgen_futures::spawn_local(async move { let _calendar_service = CalendarService::new(); // Get CalDAV password from storage let _password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; let params = event_data.to_create_event_params(); let create_result = _calendar_service.create_event( &_token, &_password, params.0, // title params.1, // description params.2, // start_date params.3, // start_time params.4, // end_date params.5, // end_time params.6, // location params.7, // all_day params.8, // status params.9, // class params.10, // priority params.11, // organizer params.12, // attendees params.13, // categories params.14, // reminder params.15, // recurrence params.16, // recurrence_days params.17 // calendar_path ).await; match create_result { Ok(_) => { web_sys::console::log_1(&"Event created successfully".into()); // Trigger a page reload to refresh events from all calendars // TODO: This could be improved to do a more targeted refresh web_sys::window().unwrap().location().reload().unwrap(); } Err(err) => { web_sys::console::error_1(&format!("Failed to create event: {}", err).into()); web_sys::window().unwrap().alert_with_message(&format!("Failed to create event: {}", err)).unwrap(); } } }); } }) }; let on_event_update = { let auth_token = auth_token.clone(); Callback::from(move |(original_event, new_start, new_end, preserve_rrule, until_date): (VEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option>)| { web_sys::console::log_1(&format!("Updating event: {} to new times: {} - {}", original_event.uid, new_start.format("%Y-%m-%d %H:%M"), new_end.format("%Y-%m-%d %H:%M")).into()); // Use the original UID for all updates let backend_uid = original_event.uid.clone(); if let Some(token) = (*auth_token).clone() { let original_event = original_event.clone(); let backend_uid = backend_uid.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); // Get CalDAV password from storage let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; // Convert local times to UTC for backend storage let start_utc = new_start.and_local_timezone(chrono::Local).unwrap().to_utc(); let end_utc = new_end.and_local_timezone(chrono::Local).unwrap().to_utc(); // Format UTC date and time strings for backend let start_date = start_utc.format("%Y-%m-%d").to_string(); let start_time = start_utc.format("%H:%M").to_string(); let end_date = end_utc.format("%Y-%m-%d").to_string(); let end_time = end_utc.format("%H:%M").to_string(); // Convert existing event data to string formats for the API let status_str = match original_event.status { Some(crate::models::ical::EventStatus::Tentative) => "TENTATIVE".to_string(), Some(crate::models::ical::EventStatus::Confirmed) => "CONFIRMED".to_string(), Some(crate::models::ical::EventStatus::Cancelled) => "CANCELLED".to_string(), None => "CONFIRMED".to_string(), // Default status }; let class_str = match original_event.class { Some(crate::models::ical::EventClass::Public) => "PUBLIC".to_string(), Some(crate::models::ical::EventClass::Private) => "PRIVATE".to_string(), Some(crate::models::ical::EventClass::Confidential) => "CONFIDENTIAL".to_string(), None => "PUBLIC".to_string(), // Default class }; // Convert reminders to string format let reminder_str = if !original_event.alarms.is_empty() { // Convert from VAlarm to minutes before "15".to_string() // TODO: Convert VAlarm trigger to minutes } else { "".to_string() }; // Handle recurrence (keep existing) let recurrence_str = original_event.rrule.unwrap_or_default(); let recurrence_days = vec![false; 7]; // Default - could be enhanced to parse existing recurrence match calendar_service.update_event( &token, &password, backend_uid, original_event.summary.unwrap_or_default(), original_event.description.unwrap_or_default(), start_date, start_time, end_date, end_time, original_event.location.unwrap_or_default(), original_event.all_day, status_str, class_str, original_event.priority, original_event.organizer.as_ref().map(|o| o.cal_address.clone()).unwrap_or_default(), original_event.attendees.iter().map(|a| a.cal_address.clone()).collect::>().join(","), original_event.categories.join(","), reminder_str, recurrence_str, recurrence_days, original_event.calendar_path, original_event.exdate.clone(), if preserve_rrule { Some("update_series".to_string()) } else { Some("this_and_future".to_string()) }, until_date ).await { Ok(_) => { web_sys::console::log_1(&"Event updated successfully".into()); // Add small delay before reload to let any pending requests complete wasm_bindgen_futures::spawn_local(async { gloo_timers::future::sleep(std::time::Duration::from_millis(100)).await; web_sys::window().unwrap().location().reload().unwrap(); }); } Err(err) => { // Check if this is a network error that occurred after success let err_str = format!("{}", err); if err_str.contains("Failed to fetch") || err_str.contains("Network request failed") { web_sys::console::log_1(&"Update may have succeeded despite network error, reloading...".into()); // Still reload as the update likely succeeded wasm_bindgen_futures::spawn_local(async { gloo_timers::future::sleep(std::time::Duration::from_millis(200)).await; web_sys::window().unwrap().location().reload().unwrap(); }); } else { web_sys::console::error_1(&format!("Failed to update event: {}", err).into()); web_sys::window().unwrap().alert_with_message(&format!("Failed to update event: {}", err)).unwrap(); } } } }); } }) }; let refresh_calendars = { let auth_token = auth_token.clone(); let user_info = user_info.clone(); Callback::from(move |_| { if let Some(token) = (*auth_token).clone() { let user_info = user_info.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; match calendar_service.fetch_user_info(&token, &password).await { Ok(mut info) => { if let Ok(saved_colors_json) = LocalStorage::get::("calendar_colors") { if let Ok(saved_info) = serde_json::from_str::(&saved_colors_json) { for saved_cal in &saved_info.calendars { for cal in &mut info.calendars { if cal.path == saved_cal.path { cal.color = saved_cal.color.clone(); } } } } } user_info.set(Some(info)); } Err(err) => { web_sys::console::log_1(&format!("Failed to refresh calendars: {}", err).into()); } } }); } }) }; html! {
{ if auth_token.is_some() { html! { <>
} } else { html! { } } } , Option)| { if let Some(token) = (*auth_token).clone() { let refresh_calendars = refresh_calendars.clone(); let create_modal_open = create_modal_open.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; match calendar_service.create_calendar(&token, &password, name, description, color).await { Ok(_) => { web_sys::console::log_1(&"Calendar created successfully!".into()); refresh_calendars.emit(()); create_modal_open.set(false); } Err(err) => { web_sys::console::log_1(&format!("Failed to create calendar: {}", err).into()); create_modal_open.set(false); } } }); } } })} available_colors={available_colors.iter().map(|c| c.to_string()).collect::>()} /> ("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; match calendar_service.delete_calendar(&token, &password, calendar_path).await { Ok(_) => { web_sys::console::log_1(&"Calendar deleted successfully!".into()); refresh_calendars.emit(()); } Err(err) => { web_sys::console::log_1(&format!("Failed to delete calendar: {}", err).into()); } } }); } } })} /> web_sys::console::log_1(&"Delete this event".into()), DeleteAction::DeleteFollowing => web_sys::console::log_1(&"Delete following events".into()), DeleteAction::DeleteSeries => web_sys::console::log_1(&"Delete entire series".into()), } wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); let password = if let Ok(credentials_str) = LocalStorage::get::("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; if let (Some(calendar_path), Some(event_href)) = (&event.calendar_path, &event.href) { // Convert delete action to string and get occurrence date let action_str = match delete_action { DeleteAction::DeleteThis => "delete_this".to_string(), DeleteAction::DeleteFollowing => "delete_following".to_string(), DeleteAction::DeleteSeries => "delete_series".to_string(), }; // Get the occurrence date from the clicked event let occurrence_date = Some(event.dtstart.date_naive().format("%Y-%m-%d").to_string()); web_sys::console::log_1(&format!("🔄 Delete action: {}", action_str).into()); web_sys::console::log_1(&format!("🔄 Event UID: {}", event.uid).into()); web_sys::console::log_1(&format!("🔄 Event start: {}", event.dtstart).into()); web_sys::console::log_1(&format!("🔄 Occurrence date: {:?}", occurrence_date).into()); match calendar_service.delete_event( &token, &password, calendar_path.clone(), event_href.clone(), action_str, occurrence_date ).await { Ok(message) => { web_sys::console::log_1(&format!("Delete response: {}", message).into()); // Show the message to the user to explain what actually happened if message.contains("Warning") { web_sys::window().unwrap().alert_with_message(&message).unwrap(); } // Close the context menu event_context_menu_open.set(false); // Force a page reload to refresh the calendar events web_sys::window().unwrap().location().reload().unwrap(); } Err(err) => { web_sys::console::log_1(&format!("Failed to delete event: {}", err).into()); web_sys::window().unwrap().alert_with_message(&format!("Failed to delete event: {}", err)).unwrap(); } } } else { web_sys::console::log_1(&"Missing calendar_path or href - cannot delete event".into()); } }); } } })} /> ("caldav_credentials") { if let Ok(credentials) = serde_json::from_str::(&credentials_str) { credentials["password"].as_str().unwrap_or("").to_string() } else { String::new() } } else { String::new() }; // Convert local times to UTC for backend storage let start_local = updated_data.start_date.and_time(updated_data.start_time); let end_local = updated_data.end_date.and_time(updated_data.end_time); 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 let status_str = match updated_data.status { EventStatus::Tentative => "tentative", EventStatus::Cancelled => "cancelled", _ => "confirmed", }.to_string(); let class_str = match updated_data.class { EventClass::Private => "private", EventClass::Confidential => "confidential", _ => "public", }.to_string(); let reminder_str = match updated_data.reminder { ReminderType::Minutes15 => "15min", ReminderType::Minutes30 => "30min", ReminderType::Hour1 => "1hour", ReminderType::Hours2 => "2hours", ReminderType::Day1 => "1day", ReminderType::Days2 => "2days", ReminderType::Week1 => "1week", _ => "none", }.to_string(); let recurrence_str = match updated_data.recurrence { RecurrenceType::Daily => "daily", RecurrenceType::Weekly => "weekly", RecurrenceType::Monthly => "monthly", RecurrenceType::Yearly => "yearly", _ => "none", }.to_string(); // Check if the calendar has changed let calendar_changed = original_event.calendar_path.as_ref() != updated_data.selected_calendar.as_ref(); if calendar_changed { // Calendar changed - need to delete from original and create in new web_sys::console::log_1(&"Calendar changed - performing delete + create".into()); // First delete from original calendar if let Some(original_calendar_path) = &original_event.calendar_path { if let Some(event_href) = &original_event.href { match calendar_service.delete_event( &token, &password, original_calendar_path.clone(), event_href.clone(), "single".to_string(), // delete single occurrence None ).await { Ok(_) => { web_sys::console::log_1(&"Original event deleted successfully".into()); // Now create the event in the new calendar match calendar_service.create_event( &token, &password, 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 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(); } } else { // Calendar hasn't changed - normal update 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, original_event.exdate.clone(), Some("update_series".to_string()), // This is for event edit modal, preserve original RRULE None // No until_date for edit modal ).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(); } } } }); } } })} available_calendars={user_info.as_ref().map(|ui| ui.calendars.clone()).unwrap_or_default()} />
} }