Compare commits
	
		
			3 Commits
		
	
	
		
			1b57adab98
			...
			197157cecb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 197157cecb | ||
|   | c273a8625a | ||
|   | 2a2666e75f | 
| @@ -7,7 +7,7 @@ use serde::Deserialize; | |||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
| use chrono::{Datelike, TimeZone}; | use chrono::{Datelike, TimeZone}; | ||||||
|  |  | ||||||
| use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse}}; | use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo, CreateCalendarRequest, CreateCalendarResponse, DeleteCalendarRequest, DeleteCalendarResponse, DeleteEventRequest, DeleteEventResponse, CreateEventRequest, CreateEventResponse, UpdateEventRequest, UpdateEventResponse}}; | ||||||
| use crate::calendar::{CalDAVClient, CalendarEvent}; | use crate::calendar::{CalDAVClient, CalendarEvent}; | ||||||
|  |  | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| @@ -763,6 +763,239 @@ pub async fn create_event( | |||||||
|     })) |     })) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | pub async fn update_event( | ||||||
|  |     State(state): State<Arc<AppState>>, | ||||||
|  |     headers: HeaderMap, | ||||||
|  |     Json(request): Json<UpdateEventRequest>, | ||||||
|  | ) -> Result<Json<UpdateEventResponse>, ApiError> { | ||||||
|  |     println!("📝 Update event request received: uid='{}', title='{}', calendar_path={:?}",  | ||||||
|  |              request.uid, request.title, request.calendar_path); | ||||||
|  |      | ||||||
|  |     // Extract and verify token | ||||||
|  |     let token = extract_bearer_token(&headers)?; | ||||||
|  |     let password = extract_password_header(&headers)?; | ||||||
|  |  | ||||||
|  |     // Validate request | ||||||
|  |     if request.uid.trim().is_empty() { | ||||||
|  |         return Err(ApiError::BadRequest("Event UID is required".to_string())); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if request.title.trim().is_empty() { | ||||||
|  |         return Err(ApiError::BadRequest("Event title is required".to_string())); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if request.title.len() > 200 { | ||||||
|  |         return Err(ApiError::BadRequest("Event title too long (max 200 characters)".to_string())); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Create CalDAV config from token and password | ||||||
|  |     let config = state.auth_service.caldav_config_from_token(&token, &password)?; | ||||||
|  |     let client = CalDAVClient::new(config); | ||||||
|  |  | ||||||
|  |     // Find the event across all calendars (or in the specified calendar) | ||||||
|  |     let calendar_paths = if let Some(path) = &request.calendar_path { | ||||||
|  |         vec![path.clone()] | ||||||
|  |     } else { | ||||||
|  |         client.discover_calendars() | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))? | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if calendar_paths.is_empty() { | ||||||
|  |         return Err(ApiError::BadRequest("No calendars available for event update".to_string())); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Search for the event by UID across the specified calendars | ||||||
|  |     let mut found_event: Option<(CalendarEvent, String, String)> = None; // (event, calendar_path, href) | ||||||
|  |     for calendar_path in &calendar_paths { | ||||||
|  |         match client.fetch_event_by_uid(calendar_path, &request.uid).await { | ||||||
|  |             Ok(Some(event)) => { | ||||||
|  |                 if let Some(href) = event.href.clone() { | ||||||
|  |                     found_event = Some((event, calendar_path.clone(), href)); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             Ok(None) => continue, // Event not found in this calendar | ||||||
|  |             Err(e) => { | ||||||
|  |                 eprintln!("Failed to fetch event from calendar {}: {}", calendar_path, e); | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let (mut event, calendar_path, event_href) = found_event | ||||||
|  |         .ok_or_else(|| ApiError::NotFound(format!("Event with UID '{}' not found", request.uid)))?; | ||||||
|  |  | ||||||
|  |     // Parse dates and times for the updated event | ||||||
|  |     let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day) | ||||||
|  |         .map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?; | ||||||
|  |      | ||||||
|  |     let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) | ||||||
|  |         .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; | ||||||
|  |  | ||||||
|  |     // Validate that end is after start | ||||||
|  |     if end_datetime <= start_datetime { | ||||||
|  |         return Err(ApiError::BadRequest("End date/time must be after start date/time".to_string())); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Parse status | ||||||
|  |     let status = match request.status.to_lowercase().as_str() { | ||||||
|  |         "tentative" => crate::calendar::EventStatus::Tentative, | ||||||
|  |         "cancelled" => crate::calendar::EventStatus::Cancelled, | ||||||
|  |         _ => crate::calendar::EventStatus::Confirmed, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Parse class | ||||||
|  |     let class = match request.class.to_lowercase().as_str() { | ||||||
|  |         "private" => crate::calendar::EventClass::Private, | ||||||
|  |         "confidential" => crate::calendar::EventClass::Confidential, | ||||||
|  |         _ => crate::calendar::EventClass::Public, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Parse attendees (comma-separated email list) | ||||||
|  |     let attendees: Vec<String> = if request.attendees.trim().is_empty() { | ||||||
|  |         Vec::new() | ||||||
|  |     } else { | ||||||
|  |         request.attendees | ||||||
|  |             .split(',') | ||||||
|  |             .map(|s| s.trim().to_string()) | ||||||
|  |             .filter(|s| !s.is_empty()) | ||||||
|  |             .collect() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Parse categories (comma-separated list) | ||||||
|  |     let categories: Vec<String> = if request.categories.trim().is_empty() { | ||||||
|  |         Vec::new() | ||||||
|  |     } else { | ||||||
|  |         request.categories | ||||||
|  |             .split(',') | ||||||
|  |             .map(|s| s.trim().to_string()) | ||||||
|  |             .filter(|s| !s.is_empty()) | ||||||
|  |             .collect() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Parse reminders and convert to EventReminder structs | ||||||
|  |     let reminders: Vec<crate::calendar::EventReminder> = match request.reminder.to_lowercase().as_str() { | ||||||
|  |         "15min" => vec![crate::calendar::EventReminder { | ||||||
|  |             minutes_before: 15, | ||||||
|  |             action: crate::calendar::ReminderAction::Display, | ||||||
|  |             description: None, | ||||||
|  |         }], | ||||||
|  |         "30min" => vec![crate::calendar::EventReminder { | ||||||
|  |             minutes_before: 30, | ||||||
|  |             action: crate::calendar::ReminderAction::Display, | ||||||
|  |             description: None, | ||||||
|  |         }], | ||||||
|  |         "1hour" => vec![crate::calendar::EventReminder { | ||||||
|  |             minutes_before: 60, | ||||||
|  |             action: crate::calendar::ReminderAction::Display, | ||||||
|  |             description: None, | ||||||
|  |         }], | ||||||
|  |         "2hours" => vec![crate::calendar::EventReminder { | ||||||
|  |             minutes_before: 120, | ||||||
|  |             action: crate::calendar::ReminderAction::Display, | ||||||
|  |             description: None, | ||||||
|  |         }], | ||||||
|  |         "1day" => vec![crate::calendar::EventReminder { | ||||||
|  |             minutes_before: 1440, // 24 * 60 | ||||||
|  |             action: crate::calendar::ReminderAction::Display, | ||||||
|  |             description: None, | ||||||
|  |         }], | ||||||
|  |         "2days" => vec![crate::calendar::EventReminder { | ||||||
|  |             minutes_before: 2880, // 48 * 60 | ||||||
|  |             action: crate::calendar::ReminderAction::Display, | ||||||
|  |             description: None, | ||||||
|  |         }], | ||||||
|  |         "1week" => vec![crate::calendar::EventReminder { | ||||||
|  |             minutes_before: 10080, // 7 * 24 * 60 | ||||||
|  |             action: crate::calendar::ReminderAction::Display, | ||||||
|  |             description: None, | ||||||
|  |         }], | ||||||
|  |         _ => Vec::new(), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Parse recurrence with BYDAY support for weekly recurrence | ||||||
|  |     let recurrence_rule = match request.recurrence.to_lowercase().as_str() { | ||||||
|  |         "daily" => Some("FREQ=DAILY".to_string()), | ||||||
|  |         "weekly" => { | ||||||
|  |             // Handle weekly recurrence with optional BYDAY parameter | ||||||
|  |             let mut rrule = "FREQ=WEEKLY".to_string(); | ||||||
|  |              | ||||||
|  |             // Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]) | ||||||
|  |             if request.recurrence_days.len() == 7 { | ||||||
|  |                 let selected_days: Vec<&str> = request.recurrence_days | ||||||
|  |                     .iter() | ||||||
|  |                     .enumerate() | ||||||
|  |                     .filter_map(|(i, &selected)| { | ||||||
|  |                         if selected { | ||||||
|  |                             Some(match i { | ||||||
|  |                                 0 => "SU", // Sunday | ||||||
|  |                                 1 => "MO", // Monday | ||||||
|  |                                 2 => "TU", // Tuesday | ||||||
|  |                                 3 => "WE", // Wednesday | ||||||
|  |                                 4 => "TH", // Thursday | ||||||
|  |                                 5 => "FR", // Friday | ||||||
|  |                                 6 => "SA", // Saturday | ||||||
|  |                                 _ => return None, | ||||||
|  |                             }) | ||||||
|  |                         } else { | ||||||
|  |                             None | ||||||
|  |                         } | ||||||
|  |                     }) | ||||||
|  |                     .collect(); | ||||||
|  |                  | ||||||
|  |                 if !selected_days.is_empty() { | ||||||
|  |                     rrule.push_str(&format!(";BYDAY={}", selected_days.join(","))); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             Some(rrule) | ||||||
|  |         }, | ||||||
|  |         "monthly" => Some("FREQ=MONTHLY".to_string()), | ||||||
|  |         "yearly" => Some("FREQ=YEARLY".to_string()), | ||||||
|  |         _ => None, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Update the event fields with new data | ||||||
|  |     event.summary = Some(request.title.clone()); | ||||||
|  |     event.description = if request.description.trim().is_empty() {  | ||||||
|  |         None  | ||||||
|  |     } else {  | ||||||
|  |         Some(request.description.clone())  | ||||||
|  |     }; | ||||||
|  |     event.start = start_datetime; | ||||||
|  |     event.end = Some(end_datetime); | ||||||
|  |     event.location = if request.location.trim().is_empty() {  | ||||||
|  |         None  | ||||||
|  |     } else {  | ||||||
|  |         Some(request.location.clone())  | ||||||
|  |     }; | ||||||
|  |     event.status = status; | ||||||
|  |     event.class = class; | ||||||
|  |     event.priority = request.priority; | ||||||
|  |     event.organizer = if request.organizer.trim().is_empty() {  | ||||||
|  |         None  | ||||||
|  |     } else {  | ||||||
|  |         Some(request.organizer.clone())  | ||||||
|  |     }; | ||||||
|  |     event.attendees = attendees; | ||||||
|  |     event.categories = categories; | ||||||
|  |     event.last_modified = Some(chrono::Utc::now()); | ||||||
|  |     event.recurrence_rule = recurrence_rule; | ||||||
|  |     event.all_day = request.all_day; | ||||||
|  |     event.reminders = reminders; | ||||||
|  |  | ||||||
|  |     // Update the event on the CalDAV server | ||||||
|  |     client.update_event(&calendar_path, &event, &event_href) | ||||||
|  |         .await | ||||||
|  |         .map_err(|e| ApiError::Internal(format!("Failed to update event: {}", e)))?; | ||||||
|  |  | ||||||
|  |     Ok(Json(UpdateEventResponse { | ||||||
|  |         success: true, | ||||||
|  |         message: "Event updated successfully".to_string(), | ||||||
|  |     })) | ||||||
|  | } | ||||||
|  |  | ||||||
| /// Parse date and time strings into a UTC DateTime | /// Parse date and time strings into a UTC DateTime | ||||||
| fn parse_event_datetime(date_str: &str, time_str: &str, all_day: bool) -> Result<chrono::DateTime<chrono::Utc>, String> { | fn parse_event_datetime(date_str: &str, time_str: &str, all_day: bool) -> Result<chrono::DateTime<chrono::Utc>, String> { | ||||||
|     use chrono::{NaiveDate, NaiveTime, NaiveDateTime, Utc, TimeZone}; |     use chrono::{NaiveDate, NaiveTime, NaiveDateTime, Utc, TimeZone}; | ||||||
|   | |||||||
| @@ -42,6 +42,7 @@ pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> { | |||||||
|         .route("/api/calendar/delete", post(handlers::delete_calendar)) |         .route("/api/calendar/delete", post(handlers::delete_calendar)) | ||||||
|         .route("/api/calendar/events", get(handlers::get_calendar_events)) |         .route("/api/calendar/events", get(handlers::get_calendar_events)) | ||||||
|         .route("/api/calendar/events/create", post(handlers::create_event)) |         .route("/api/calendar/events/create", post(handlers::create_event)) | ||||||
|  |         .route("/api/calendar/events/update", post(handlers::update_event)) | ||||||
|         .route("/api/calendar/events/delete", post(handlers::delete_event)) |         .route("/api/calendar/events/delete", post(handlers::delete_event)) | ||||||
|         .route("/api/calendar/events/:uid", get(handlers::refresh_event)) |         .route("/api/calendar/events/:uid", get(handlers::refresh_event)) | ||||||
|         .layer( |         .layer( | ||||||
|   | |||||||
| @@ -101,6 +101,35 @@ pub struct CreateEventResponse { | |||||||
|     pub event_href: Option<String>, // The created event's href/filename |     pub event_href: Option<String>, // The created event's href/filename | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Deserialize)] | ||||||
|  | pub struct UpdateEventRequest { | ||||||
|  |     pub uid: String,               // Event UID to identify which event to update | ||||||
|  |     pub title: String, | ||||||
|  |     pub description: String, | ||||||
|  |     pub start_date: String,        // YYYY-MM-DD format | ||||||
|  |     pub start_time: String,        // HH:MM format   | ||||||
|  |     pub end_date: String,          // YYYY-MM-DD format | ||||||
|  |     pub end_time: String,          // HH:MM format | ||||||
|  |     pub location: String, | ||||||
|  |     pub all_day: bool, | ||||||
|  |     pub status: String,            // confirmed, tentative, cancelled | ||||||
|  |     pub class: String,             // public, private, confidential | ||||||
|  |     pub priority: Option<u8>,      // 0-9 priority level | ||||||
|  |     pub organizer: String,         // organizer email | ||||||
|  |     pub attendees: String,         // comma-separated attendee emails | ||||||
|  |     pub categories: String,        // comma-separated categories | ||||||
|  |     pub reminder: String,          // reminder type | ||||||
|  |     pub recurrence: String,        // recurrence type | ||||||
|  |     pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence | ||||||
|  |     pub calendar_path: Option<String>, // Optional - search all calendars if not specified | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize)] | ||||||
|  | pub struct UpdateEventResponse { | ||||||
|  |     pub success: bool, | ||||||
|  |     pub message: String, | ||||||
|  | } | ||||||
|  |  | ||||||
| // Error handling | // Error handling | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub enum ApiError { | pub enum ApiError { | ||||||
|   | |||||||
							
								
								
									
										117
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								src/app.rs
									
									
									
									
									
								
							| @@ -480,6 +480,16 @@ pub fn App() -> Html { | |||||||
|                         let event_context_menu_open = event_context_menu_open.clone(); |                         let event_context_menu_open = event_context_menu_open.clone(); | ||||||
|                         move |_| event_context_menu_open.set(false) |                         move |_| event_context_menu_open.set(false) | ||||||
|                     })} |                     })} | ||||||
|  |                     on_edit={Callback::from({ | ||||||
|  |                         let _event_context_menu_event = event_context_menu_event.clone(); | ||||||
|  |                         let event_context_menu_open = event_context_menu_open.clone(); | ||||||
|  |                         let create_event_modal_open = create_event_modal_open.clone(); | ||||||
|  |                         move |_| { | ||||||
|  |                             // Close the context menu and open the edit modal | ||||||
|  |                             event_context_menu_open.set(false); | ||||||
|  |                             create_event_modal_open.set(true); | ||||||
|  |                         } | ||||||
|  |                     })} | ||||||
|                     on_delete={Callback::from({ |                     on_delete={Callback::from({ | ||||||
|                         let auth_token = auth_token.clone(); |                         let auth_token = auth_token.clone(); | ||||||
|                         let event_context_menu_event = event_context_menu_event.clone(); |                         let event_context_menu_event = event_context_menu_event.clone(); | ||||||
| @@ -575,11 +585,116 @@ pub fn App() -> Html { | |||||||
|                 <CreateEventModal  |                 <CreateEventModal  | ||||||
|                     is_open={*create_event_modal_open} |                     is_open={*create_event_modal_open} | ||||||
|                     selected_date={(*selected_date_for_event).clone()} |                     selected_date={(*selected_date_for_event).clone()} | ||||||
|  |                     event_to_edit={(*event_context_menu_event).clone()} | ||||||
|                     on_close={Callback::from({ |                     on_close={Callback::from({ | ||||||
|                         let create_event_modal_open = create_event_modal_open.clone(); |                         let create_event_modal_open = create_event_modal_open.clone(); | ||||||
|                         move |_| create_event_modal_open.set(false) |                         let event_context_menu_event = event_context_menu_event.clone(); | ||||||
|  |                         move |_| { | ||||||
|  |                             create_event_modal_open.set(false); | ||||||
|  |                             // Clear the event being edited | ||||||
|  |                             event_context_menu_event.set(None); | ||||||
|  |                         } | ||||||
|                     })} |                     })} | ||||||
|                     on_create={on_event_create} |                     on_create={on_event_create} | ||||||
|  |                     on_update={Callback::from({ | ||||||
|  |                         let auth_token = auth_token.clone(); | ||||||
|  |                         let create_event_modal_open = create_event_modal_open.clone(); | ||||||
|  |                         let event_context_menu_event = event_context_menu_event.clone(); | ||||||
|  |                         move |(original_event, updated_data): (CalendarEvent, EventCreationData)| { | ||||||
|  |                             web_sys::console::log_1(&format!("Updating event: {:?}", updated_data).into()); | ||||||
|  |                             create_event_modal_open.set(false); | ||||||
|  |                             event_context_menu_event.set(None); | ||||||
|  |                              | ||||||
|  |                             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::<String>("caldav_credentials") { | ||||||
|  |                                         if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) { | ||||||
|  |                                             credentials["password"].as_str().unwrap_or("").to_string() | ||||||
|  |                                         } else { | ||||||
|  |                                             String::new() | ||||||
|  |                                         } | ||||||
|  |                                     } else { | ||||||
|  |                                         String::new() | ||||||
|  |                                     }; | ||||||
|  |                                      | ||||||
|  |                                     // Format date and time strings | ||||||
|  |                                     let start_date = updated_data.start_date.format("%Y-%m-%d").to_string(); | ||||||
|  |                                     let start_time = updated_data.start_time.format("%H:%M").to_string(); | ||||||
|  |                                     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(); | ||||||
|  |                                      | ||||||
|  |                                     // 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(); | ||||||
|  |  | ||||||
|  |                                     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(); | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 }); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     })} | ||||||
|                     available_calendars={user_info.as_ref().map(|ui| ui.calendars.clone()).unwrap_or_default()} |                     available_calendars={user_info.as_ref().map(|ui| ui.calendars.clone()).unwrap_or_default()} | ||||||
|                 /> |                 /> | ||||||
|             </div> |             </div> | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ use std::collections::HashMap; | |||||||
| use crate::services::calendar_service::{CalendarEvent, UserInfo}; | use crate::services::calendar_service::{CalendarEvent, UserInfo}; | ||||||
| use crate::components::EventModal; | use crate::components::EventModal; | ||||||
| use wasm_bindgen::JsCast; | use wasm_bindgen::JsCast; | ||||||
|  | use gloo_storage::{LocalStorage, Storage}; | ||||||
|  |  | ||||||
| #[derive(Properties, PartialEq)] | #[derive(Properties, PartialEq)] | ||||||
| pub struct CalendarProps { | pub struct CalendarProps { | ||||||
| @@ -23,7 +24,19 @@ pub struct CalendarProps { | |||||||
| #[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(|| today); |     let current_month = use_state(|| { | ||||||
|  |         // Try to load saved month from localStorage | ||||||
|  |         if let Ok(saved_month_str) = LocalStorage::get::<String>("calendar_current_month") { | ||||||
|  |             if let Ok(saved_month) = NaiveDate::parse_from_str(&saved_month_str, "%Y-%m-%d") { | ||||||
|  |                 // Return the first day of the saved month | ||||||
|  |                 saved_month.with_day(1).unwrap_or(today) | ||||||
|  |             } else { | ||||||
|  |                 today | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             today | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|     let selected_day = use_state(|| today); |     let selected_day = use_state(|| today); | ||||||
|     let selected_event = use_state(|| None::<CalendarEvent>); |     let selected_event = use_state(|| None::<CalendarEvent>); | ||||||
|      |      | ||||||
| @@ -53,6 +66,8 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|             let prev = *current_month - Duration::days(1); |             let prev = *current_month - Duration::days(1); | ||||||
|             let first_of_prev = prev.with_day(1).unwrap(); |             let first_of_prev = prev.with_day(1).unwrap(); | ||||||
|             current_month.set(first_of_prev); |             current_month.set(first_of_prev); | ||||||
|  |             // Save to localStorage | ||||||
|  |             let _ = LocalStorage::set("calendar_current_month", first_of_prev.format("%Y-%m-%d").to_string()); | ||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
| @@ -65,6 +80,19 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|                 NaiveDate::from_ymd_opt(current_month.year(), current_month.month() + 1, 1).unwrap() |                 NaiveDate::from_ymd_opt(current_month.year(), current_month.month() + 1, 1).unwrap() | ||||||
|             }; |             }; | ||||||
|             current_month.set(next); |             current_month.set(next); | ||||||
|  |             // Save to localStorage | ||||||
|  |             let _ = LocalStorage::set("calendar_current_month", next.format("%Y-%m-%d").to_string()); | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let go_to_today = { | ||||||
|  |         let current_month = current_month.clone(); | ||||||
|  |         Callback::from(move |_| { | ||||||
|  |             let today = Local::now().date_naive(); | ||||||
|  |             let first_of_today_month = today.with_day(1).unwrap(); | ||||||
|  |             current_month.set(first_of_today_month); | ||||||
|  |             // Save to localStorage | ||||||
|  |             let _ = LocalStorage::set("calendar_current_month", first_of_today_month.format("%Y-%m-%d").to_string()); | ||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
| @@ -73,8 +101,11 @@ pub fn Calendar(props: &CalendarProps) -> Html { | |||||||
|             <div class="calendar-header"> |             <div class="calendar-header"> | ||||||
|                 <button class="nav-button" onclick={prev_month}>{"‹"}</button> |                 <button class="nav-button" onclick={prev_month}>{"‹"}</button> | ||||||
|                 <h2 class="month-year">{format!("{} {}", get_month_name(current_month.month()), current_month.year())}</h2> |                 <h2 class="month-year">{format!("{} {}", get_month_name(current_month.month()), current_month.year())}</h2> | ||||||
|  |                 <div class="header-right"> | ||||||
|  |                     <button class="today-button" onclick={go_to_today}>{"Today"}</button> | ||||||
|                     <button class="nav-button" onclick={next_month}>{"›"}</button> |                     <button class="nav-button" onclick={next_month}>{"›"}</button> | ||||||
|                 </div> |                 </div> | ||||||
|  |             </div> | ||||||
|              |              | ||||||
|             <div class="calendar-grid"> |             <div class="calendar-grid"> | ||||||
|                 // Weekday headers |                 // Weekday headers | ||||||
|   | |||||||
| @@ -1,14 +1,16 @@ | |||||||
| use yew::prelude::*; | use yew::prelude::*; | ||||||
| use web_sys::{HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement}; | use web_sys::{HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement}; | ||||||
| use chrono::{NaiveDate, NaiveTime}; | use chrono::{NaiveDate, NaiveTime}; | ||||||
| use crate::services::calendar_service::CalendarInfo; | use crate::services::calendar_service::{CalendarInfo, CalendarEvent}; | ||||||
|  |  | ||||||
| #[derive(Properties, PartialEq)] | #[derive(Properties, PartialEq)] | ||||||
| pub struct CreateEventModalProps { | pub struct CreateEventModalProps { | ||||||
|     pub is_open: bool, |     pub is_open: bool, | ||||||
|     pub selected_date: Option<NaiveDate>, |     pub selected_date: Option<NaiveDate>, | ||||||
|  |     pub event_to_edit: Option<CalendarEvent>, | ||||||
|     pub on_close: Callback<()>, |     pub on_close: Callback<()>, | ||||||
|     pub on_create: Callback<EventCreationData>, |     pub on_create: Callback<EventCreationData>, | ||||||
|  |     pub on_update: Callback<(CalendarEvent, EventCreationData)>, // (original_event, updated_data) | ||||||
|     pub available_calendars: Vec<CalendarInfo>, |     pub available_calendars: Vec<CalendarInfo>, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -25,6 +27,16 @@ impl Default for EventStatus { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl EventStatus { | ||||||
|  |     pub fn from_service_status(status: &crate::services::calendar_service::EventStatus) -> Self { | ||||||
|  |         match status { | ||||||
|  |             crate::services::calendar_service::EventStatus::Tentative => EventStatus::Tentative, | ||||||
|  |             crate::services::calendar_service::EventStatus::Confirmed => EventStatus::Confirmed, | ||||||
|  |             crate::services::calendar_service::EventStatus::Cancelled => EventStatus::Cancelled, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Clone, PartialEq, Debug)] | #[derive(Clone, PartialEq, Debug)] | ||||||
| pub enum EventClass { | pub enum EventClass { | ||||||
|     Public, |     Public, | ||||||
| @@ -38,6 +50,16 @@ impl Default for EventClass { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl EventClass { | ||||||
|  |     pub fn from_service_class(class: &crate::services::calendar_service::EventClass) -> Self { | ||||||
|  |         match class { | ||||||
|  |             crate::services::calendar_service::EventClass::Public => EventClass::Public, | ||||||
|  |             crate::services::calendar_service::EventClass::Private => EventClass::Private, | ||||||
|  |             crate::services::calendar_service::EventClass::Confidential => EventClass::Confidential, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Clone, PartialEq, Debug)] | #[derive(Clone, PartialEq, Debug)] | ||||||
| pub enum ReminderType { | pub enum ReminderType { | ||||||
|     None, |     None, | ||||||
| @@ -71,6 +93,18 @@ impl Default for RecurrenceType { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl RecurrenceType { | ||||||
|  |     pub fn from_rrule(rrule: Option<&str>) -> Self { | ||||||
|  |         match rrule { | ||||||
|  |             Some(rule) if rule.contains("FREQ=DAILY") => RecurrenceType::Daily, | ||||||
|  |             Some(rule) if rule.contains("FREQ=WEEKLY") => RecurrenceType::Weekly, | ||||||
|  |             Some(rule) if rule.contains("FREQ=MONTHLY") => RecurrenceType::Monthly, | ||||||
|  |             Some(rule) if rule.contains("FREQ=YEARLY") => RecurrenceType::Yearly, | ||||||
|  |             _ => RecurrenceType::None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[derive(Clone, PartialEq, Debug)] | #[derive(Clone, PartialEq, Debug)] | ||||||
| pub struct EventCreationData { | pub struct EventCreationData { | ||||||
|     pub title: String, |     pub title: String, | ||||||
| @@ -122,25 +156,56 @@ impl Default for EventCreationData { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl EventCreationData { | ||||||
|  |     pub fn from_calendar_event(event: &CalendarEvent) -> Self { | ||||||
|  |         // Convert CalendarEvent to EventCreationData for editing | ||||||
|  |         Self { | ||||||
|  |             title: event.summary.clone().unwrap_or_default(), | ||||||
|  |             description: event.description.clone().unwrap_or_default(), | ||||||
|  |             start_date: event.start.date_naive(), | ||||||
|  |             start_time: event.start.time(), | ||||||
|  |             end_date: event.end.as_ref().map(|e| e.date_naive()).unwrap_or(event.start.date_naive()), | ||||||
|  |             end_time: event.end.as_ref().map(|e| e.time()).unwrap_or(event.start.time()), | ||||||
|  |             location: event.location.clone().unwrap_or_default(), | ||||||
|  |             all_day: event.all_day, | ||||||
|  |             status: EventStatus::from_service_status(&event.status), | ||||||
|  |             class: EventClass::from_service_class(&event.class), | ||||||
|  |             priority: event.priority, | ||||||
|  |             organizer: event.organizer.clone().unwrap_or_default(), | ||||||
|  |             attendees: event.attendees.join(", "), | ||||||
|  |             categories: event.categories.join(", "), | ||||||
|  |             reminder: ReminderType::default(), // TODO: Convert from event reminders | ||||||
|  |             recurrence: RecurrenceType::from_rrule(event.recurrence_rule.as_deref()), | ||||||
|  |             recurrence_days: vec![false; 7], // TODO: Parse from RRULE | ||||||
|  |             selected_calendar: event.calendar_path.clone(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| #[function_component(CreateEventModal)] | #[function_component(CreateEventModal)] | ||||||
| pub fn create_event_modal(props: &CreateEventModalProps) -> Html { | 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 if provided |     // Initialize with selected date or event data if provided | ||||||
|     use_effect_with((props.selected_date, 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()), { | ||||||
|         let event_data = event_data.clone(); |         let event_data = event_data.clone(); | ||||||
|         move |(selected_date, is_open, available_calendars)| { |         move |(selected_date, event_to_edit, is_open, available_calendars)| { | ||||||
|             if *is_open { |             if *is_open { | ||||||
|                 let mut data = if let Some(date) = selected_date { |                 let mut data = if let Some(event) = event_to_edit { | ||||||
|                     let mut data = (*event_data).clone(); |                     // Pre-populate with event data for editing | ||||||
|  |                     EventCreationData::from_calendar_event(event) | ||||||
|  |                 } else if let Some(date) = selected_date { | ||||||
|  |                     // Initialize with selected date for new event | ||||||
|  |                     let mut data = EventCreationData::default(); | ||||||
|                     data.start_date = *date; |                     data.start_date = *date; | ||||||
|                     data.end_date = *date; |                     data.end_date = *date; | ||||||
|                     data |                     data | ||||||
|                 } else { |                 } else { | ||||||
|  |                     // Default initialization | ||||||
|                     EventCreationData::default() |                     EventCreationData::default() | ||||||
|                 }; |                 }; | ||||||
|                  |                  | ||||||
|                 // Set default calendar to the first available one |                 // Set default calendar to the first available one if none selected | ||||||
|                 if data.selected_calendar.is_none() && !available_calendars.is_empty() { |                 if data.selected_calendar.is_none() && !available_calendars.is_empty() { | ||||||
|                     data.selected_calendar = Some(available_calendars[0].path.clone()); |                     data.selected_calendar = Some(available_calendars[0].path.clone()); | ||||||
|                 } |                 } | ||||||
| @@ -401,11 +466,19 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { | |||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     let on_create_click = { |     let on_submit_click = { | ||||||
|         let event_data = event_data.clone(); |         let event_data = event_data.clone(); | ||||||
|         let on_create = props.on_create.clone(); |         let on_create = props.on_create.clone(); | ||||||
|  |         let on_update = props.on_update.clone(); | ||||||
|  |         let event_to_edit = props.event_to_edit.clone(); | ||||||
|         Callback::from(move |_: MouseEvent| { |         Callback::from(move |_: MouseEvent| { | ||||||
|  |             if let Some(original_event) = &event_to_edit { | ||||||
|  |                 // We're editing - call on_update with original event and new data | ||||||
|  |                 on_update.emit((original_event.clone(), (*event_data).clone())); | ||||||
|  |             } else { | ||||||
|  |                 // We're creating - call on_create with new data | ||||||
|                 on_create.emit((*event_data).clone()); |                 on_create.emit((*event_data).clone()); | ||||||
|  |             } | ||||||
|         }) |         }) | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -422,7 +495,7 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { | |||||||
|         <div class="modal-backdrop" onclick={on_backdrop_click}> |         <div class="modal-backdrop" onclick={on_backdrop_click}> | ||||||
|             <div class="modal-content create-event-modal" onclick={Callback::from(|e: MouseEvent| e.stop_propagation())}> |             <div class="modal-content create-event-modal" onclick={Callback::from(|e: MouseEvent| e.stop_propagation())}> | ||||||
|                 <div class="modal-header"> |                 <div class="modal-header"> | ||||||
|                     <h3>{"Create New Event"}</h3> |                     <h3>{if props.event_to_edit.is_some() { "Edit Event" } else { "Create New Event" }}</h3> | ||||||
|                     <button type="button" class="modal-close" onclick={Callback::from({ |                     <button type="button" class="modal-close" onclick={Callback::from({ | ||||||
|                         let on_close = props.on_close.clone(); |                         let on_close = props.on_close.clone(); | ||||||
|                         move |_: MouseEvent| on_close.emit(()) |                         move |_: MouseEvent| on_close.emit(()) | ||||||
| @@ -707,10 +780,10 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { | |||||||
|                     <button  |                     <button  | ||||||
|                         type="button"  |                         type="button"  | ||||||
|                         class="btn btn-primary"  |                         class="btn btn-primary"  | ||||||
|                         onclick={on_create_click} |                         onclick={on_submit_click} | ||||||
|                         disabled={data.title.trim().is_empty()} |                         disabled={data.title.trim().is_empty()} | ||||||
|                     > |                     > | ||||||
|                         {"Create Event"} |                         {if props.event_to_edit.is_some() { "Update Event" } else { "Create Event" }} | ||||||
|                     </button> |                     </button> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ pub struct EventContextMenuProps { | |||||||
|     pub x: i32, |     pub x: i32, | ||||||
|     pub y: i32, |     pub y: i32, | ||||||
|     pub event: Option<CalendarEvent>, |     pub event: Option<CalendarEvent>, | ||||||
|  |     pub on_edit: Callback<()>, | ||||||
|     pub on_delete: Callback<DeleteAction>, |     pub on_delete: Callback<DeleteAction>, | ||||||
|     pub on_close: Callback<()>, |     pub on_close: Callback<()>, | ||||||
| } | } | ||||||
| @@ -37,6 +38,15 @@ pub fn event_context_menu(props: &EventContextMenuProps) -> Html { | |||||||
|         .map(|event| event.recurrence_rule.is_some()) |         .map(|event| event.recurrence_rule.is_some()) | ||||||
|         .unwrap_or(false); |         .unwrap_or(false); | ||||||
|  |  | ||||||
|  |     let on_edit_click = { | ||||||
|  |         let on_edit = props.on_edit.clone(); | ||||||
|  |         let on_close = props.on_close.clone(); | ||||||
|  |         Callback::from(move |_: MouseEvent| { | ||||||
|  |             on_edit.emit(()); | ||||||
|  |             on_close.emit(()); | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     let create_delete_callback = |action: DeleteAction| { |     let create_delete_callback = |action: DeleteAction| { | ||||||
|         let on_delete = props.on_delete.clone(); |         let on_delete = props.on_delete.clone(); | ||||||
|         let on_close = props.on_close.clone(); |         let on_close = props.on_close.clone(); | ||||||
| @@ -52,6 +62,10 @@ pub fn event_context_menu(props: &EventContextMenuProps) -> Html { | |||||||
|             class="context-menu"  |             class="context-menu"  | ||||||
|             style={style} |             style={style} | ||||||
|         > |         > | ||||||
|  |             <div class="context-menu-item" onclick={on_edit_click}> | ||||||
|  |                 <span class="context-menu-icon">{"✏️"}</span> | ||||||
|  |                 {"Edit Event"} | ||||||
|  |             </div> | ||||||
|             { |             { | ||||||
|                 if is_recurring { |                 if is_recurring { | ||||||
|                     html! { |                     html! { | ||||||
|   | |||||||
| @@ -755,6 +755,97 @@ impl CalendarService { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub async fn update_event( | ||||||
|  |         &self, | ||||||
|  |         token: &str, | ||||||
|  |         password: &str, | ||||||
|  |         event_uid: String, | ||||||
|  |         title: String, | ||||||
|  |         description: String, | ||||||
|  |         start_date: String, | ||||||
|  |         start_time: String, | ||||||
|  |         end_date: String, | ||||||
|  |         end_time: String, | ||||||
|  |         location: String, | ||||||
|  |         all_day: bool, | ||||||
|  |         status: String, | ||||||
|  |         class: String, | ||||||
|  |         priority: Option<u8>, | ||||||
|  |         organizer: String, | ||||||
|  |         attendees: String, | ||||||
|  |         categories: String, | ||||||
|  |         reminder: String, | ||||||
|  |         recurrence: String, | ||||||
|  |         recurrence_days: Vec<bool>, | ||||||
|  |         calendar_path: Option<String> | ||||||
|  |     ) -> Result<(), String> { | ||||||
|  |         let window = web_sys::window().ok_or("No global window exists")?; | ||||||
|  |          | ||||||
|  |         let opts = RequestInit::new(); | ||||||
|  |         opts.set_method("POST"); | ||||||
|  |         opts.set_mode(RequestMode::Cors); | ||||||
|  |  | ||||||
|  |         let body = serde_json::json!({ | ||||||
|  |             "uid": event_uid, | ||||||
|  |             "title": title, | ||||||
|  |             "description": description, | ||||||
|  |             "start_date": start_date, | ||||||
|  |             "start_time": start_time, | ||||||
|  |             "end_date": end_date, | ||||||
|  |             "end_time": end_time, | ||||||
|  |             "location": location, | ||||||
|  |             "all_day": all_day, | ||||||
|  |             "status": status, | ||||||
|  |             "class": class, | ||||||
|  |             "priority": priority, | ||||||
|  |             "organizer": organizer, | ||||||
|  |             "attendees": attendees, | ||||||
|  |             "categories": categories, | ||||||
|  |             "reminder": reminder, | ||||||
|  |             "recurrence": recurrence, | ||||||
|  |             "recurrence_days": recurrence_days, | ||||||
|  |             "calendar_path": calendar_path | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         let body_string = serde_json::to_string(&body) | ||||||
|  |             .map_err(|e| format!("JSON serialization failed: {}", e))?; | ||||||
|  |  | ||||||
|  |         let url = format!("{}/calendar/events/update", self.base_url); | ||||||
|  |         opts.set_body(&body_string.into()); | ||||||
|  |         let request = Request::new_with_str_and_init(&url, &opts) | ||||||
|  |             .map_err(|e| format!("Request creation failed: {:?}", e))?; | ||||||
|  |  | ||||||
|  |         request.headers().set("Authorization", &format!("Bearer {}", token)) | ||||||
|  |             .map_err(|e| format!("Authorization header setting failed: {:?}", e))?; | ||||||
|  |          | ||||||
|  |         request.headers().set("X-CalDAV-Password", password) | ||||||
|  |             .map_err(|e| format!("Password header setting failed: {:?}", e))?; | ||||||
|  |  | ||||||
|  |         request.headers().set("Content-Type", "application/json") | ||||||
|  |             .map_err(|e| format!("Content-Type header setting failed: {:?}", e))?; | ||||||
|  |  | ||||||
|  |         let resp_value = JsFuture::from(window.fetch_with_request(&request)) | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| format!("Network request failed: {:?}", e))?; | ||||||
|  |  | ||||||
|  |         let resp: Response = resp_value.dyn_into() | ||||||
|  |             .map_err(|e| format!("Response cast failed: {:?}", e))?; | ||||||
|  |  | ||||||
|  |         let text = JsFuture::from(resp.text() | ||||||
|  |             .map_err(|e| format!("Text extraction failed: {:?}", e))?) | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| format!("Text promise failed: {:?}", e))?; | ||||||
|  |  | ||||||
|  |         let text_string = text.as_string() | ||||||
|  |             .ok_or("Response text is not a string")?; | ||||||
|  |  | ||||||
|  |         if resp.ok() { | ||||||
|  |             Ok(()) | ||||||
|  |         } else { | ||||||
|  |             Err(format!("Request failed with status {}: {}", resp.status(), text_string)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /// Delete a calendar from the CalDAV server |     /// Delete a calendar from the CalDAV server | ||||||
|     pub async fn delete_calendar( |     pub async fn delete_calendar( | ||||||
|         &self,  |         &self,  | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								styles.css
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								styles.css
									
									
									
									
									
								
							| @@ -405,6 +405,15 @@ body { | |||||||
|     font-size: 1.8rem; |     font-size: 1.8rem; | ||||||
|     font-weight: 600; |     font-weight: 600; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|  |     position: absolute; | ||||||
|  |     left: 50%; | ||||||
|  |     transform: translateX(-50%); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .header-right { | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     gap: 0.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| .nav-button { | .nav-button { | ||||||
| @@ -427,6 +436,25 @@ body { | |||||||
|     background: rgba(255,255,255,0.3); |     background: rgba(255,255,255,0.3); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .today-button { | ||||||
|  |     background: rgba(255,255,255,0.2); | ||||||
|  |     border: none; | ||||||
|  |     color: white; | ||||||
|  |     font-size: 0.9rem; | ||||||
|  |     font-weight: 500; | ||||||
|  |     padding: 0.5rem 1rem; | ||||||
|  |     border-radius: 20px; | ||||||
|  |     cursor: pointer; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     transition: background-color 0.2s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .today-button:hover { | ||||||
|  |     background: rgba(255,255,255,0.3); | ||||||
|  | } | ||||||
|  |  | ||||||
| .calendar-grid { | .calendar-grid { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-columns: repeat(7, 1fr); |     grid-template-columns: repeat(7, 1fr); | ||||||
| @@ -766,6 +794,11 @@ body { | |||||||
|         font-size: 1.2rem; |         font-size: 1.2rem; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     .today-button { | ||||||
|  |         font-size: 0.8rem; | ||||||
|  |         padding: 0.4rem 0.8rem; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     .weekday-header { |     .weekday-header { | ||||||
|         padding: 0.5rem; |         padding: 0.5rem; | ||||||
|         font-size: 0.8rem; |         font-size: 0.8rem; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user