Implement complete CalDAV events integration
Added full CalDAV integration to display real calendar events from Baikal server: Backend changes: - Added CalDAV client with iCalendar parsing and XML handling - Created /api/calendar/events endpoint with authentication - Fixed multiline regex patterns for namespace-prefixed XML tags - Added proper calendar path discovery and event filtering Frontend changes: - Created CalendarService for API communication - Updated calendar UI to display events as blue boxes with titles - Added loading states and error handling - Integrated real-time event fetching on calendar load Now successfully displays 3 test events from the Baikal server with proper date formatting and responsive design. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										72
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								src/app.rs
									
									
									
									
									
								
							| @@ -2,7 +2,9 @@ use yew::prelude::*; | ||||
| use yew_router::prelude::*; | ||||
| use gloo_storage::{LocalStorage, Storage}; | ||||
| use crate::components::{Login, Register, Calendar}; | ||||
| use crate::services::CalendarService; | ||||
| use std::collections::HashMap; | ||||
| use chrono::{Local, NaiveDate, Datelike}; | ||||
|  | ||||
| #[derive(Clone, Routable, PartialEq)] | ||||
| enum Route { | ||||
| @@ -105,12 +107,76 @@ pub fn App() -> Html { | ||||
|  | ||||
| #[function_component] | ||||
| fn CalendarView() -> Html { | ||||
|     // Sample events for demonstration | ||||
|     let events = HashMap::new(); | ||||
|     let events = use_state(|| HashMap::<NaiveDate, Vec<String>>::new()); | ||||
|     let loading = use_state(|| true); | ||||
|     let error = use_state(|| None::<String>); | ||||
|      | ||||
|     // Get current auth token | ||||
|     let auth_token: Option<String> = LocalStorage::get("auth_token").ok(); | ||||
|      | ||||
|     let today = Local::now().date_naive(); | ||||
|     let current_year = today.year(); | ||||
|     let current_month = today.month(); | ||||
|      | ||||
|     // Fetch events when component mounts | ||||
|     { | ||||
|         let events = events.clone(); | ||||
|         let loading = loading.clone(); | ||||
|         let error = error.clone(); | ||||
|         let auth_token = auth_token.clone(); | ||||
|          | ||||
|         use_effect_with((), move |_| { | ||||
|             if let Some(token) = auth_token { | ||||
|                 let events = events.clone(); | ||||
|                 let loading = loading.clone(); | ||||
|                 let error = error.clone(); | ||||
|                  | ||||
|                 wasm_bindgen_futures::spawn_local(async move { | ||||
|                     let calendar_service = CalendarService::new(); | ||||
|                      | ||||
|                     match calendar_service.fetch_events_for_month(&token, current_year, current_month).await { | ||||
|                         Ok(calendar_events) => { | ||||
|                             let grouped_events = CalendarService::group_events_by_date(calendar_events); | ||||
|                             events.set(grouped_events); | ||||
|                             loading.set(false); | ||||
|                         } | ||||
|                         Err(err) => { | ||||
|                             error.set(Some(format!("Failed to load events: {}", err))); | ||||
|                             loading.set(false); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|             } else { | ||||
|                 loading.set(false); | ||||
|                 error.set(Some("No authentication token found".to_string())); | ||||
|             } | ||||
|              | ||||
|             || () | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     html! { | ||||
|         <div class="calendar-view"> | ||||
|             <Calendar events={events} /> | ||||
|             { | ||||
|                 if *loading { | ||||
|                     html! { | ||||
|                         <div class="calendar-loading"> | ||||
|                             <p>{"Loading calendar events..."}</p> | ||||
|                         </div> | ||||
|                     } | ||||
|                 } else if let Some(err) = (*error).clone() { | ||||
|                     html! { | ||||
|                         <div class="calendar-error"> | ||||
|                             <p>{format!("Error: {}", err)}</p> | ||||
|                             <Calendar events={HashMap::new()} /> | ||||
|                         </div> | ||||
|                     } | ||||
|                 } else { | ||||
|                     html! { | ||||
|                         <Calendar events={(*events).clone()} /> | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         </div> | ||||
|     } | ||||
| } | ||||
| @@ -89,13 +89,23 @@ pub fn Calendar(props: &CalendarProps) -> Html { | ||||
|                                         html! { | ||||
|                                             <div class="event-indicators"> | ||||
|                                                 { | ||||
|                                                     events.iter().take(3).map(|event| { | ||||
|                                                         html! { <div class="event-dot" title={event.clone()}></div> } | ||||
|                                                     events.iter().take(2).map(|event| { | ||||
|                                                         html! {  | ||||
|                                                             <div class="event-box" title={event.clone()}> | ||||
|                                                                 { | ||||
|                                                                     if event.len() > 15 { | ||||
|                                                                         format!("{}...", &event[..12]) | ||||
|                                                                     } else { | ||||
|                                                                         event.clone() | ||||
|                                                                     } | ||||
|                                                                 } | ||||
|                                                             </div>  | ||||
|                                                         } | ||||
|                                                     }).collect::<Html>() | ||||
|                                                 } | ||||
|                                                 { | ||||
|                                                     if events.len() > 3 { | ||||
|                                                         html! { <div class="more-events">{format!("+{}", events.len() - 3)}</div> } | ||||
|                                                     if events.len() > 2 { | ||||
|                                                         html! { <div class="more-events">{format!("+{} more", events.len() - 2)}</div> } | ||||
|                                                     } else { | ||||
|                                                         html! {} | ||||
|                                                     } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| mod app; | ||||
| mod auth; | ||||
| mod components; | ||||
| mod services; | ||||
|  | ||||
| use app::App; | ||||
|  | ||||
|   | ||||
							
								
								
									
										108
									
								
								src/services/calendar_service.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/services/calendar_service.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| use chrono::{DateTime, Utc, NaiveDate}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use wasm_bindgen::JsCast; | ||||
| use wasm_bindgen_futures::JsFuture; | ||||
| use web_sys::{Request, RequestInit, RequestMode, Response}; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct CalendarEvent { | ||||
|     pub uid: String, | ||||
|     pub summary: Option<String>, | ||||
|     pub description: Option<String>, | ||||
|     pub start: DateTime<Utc>, | ||||
|     pub end: Option<DateTime<Utc>>, | ||||
|     pub location: Option<String>, | ||||
|     pub status: String, | ||||
|     pub all_day: bool, | ||||
| } | ||||
|  | ||||
| impl CalendarEvent { | ||||
|     /// Get the date for this event (for calendar display) | ||||
|     pub fn get_date(&self) -> NaiveDate { | ||||
|         if self.all_day { | ||||
|             self.start.date_naive() | ||||
|         } else { | ||||
|             self.start.date_naive() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Get display title for the event | ||||
|     pub fn get_title(&self) -> String { | ||||
|         self.summary.clone().unwrap_or_else(|| "Untitled Event".to_string()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct CalendarService { | ||||
|     base_url: String, | ||||
| } | ||||
|  | ||||
| impl CalendarService { | ||||
|     pub fn new() -> Self { | ||||
|         let base_url = option_env!("BACKEND_API_URL") | ||||
|             .unwrap_or("http://localhost:3000/api") | ||||
|             .to_string(); | ||||
|          | ||||
|         Self { base_url } | ||||
|     } | ||||
|  | ||||
|     /// Fetch calendar events for a specific month | ||||
|     pub async fn fetch_events_for_month( | ||||
|         &self,  | ||||
|         token: &str,  | ||||
|         year: i32,  | ||||
|         month: u32 | ||||
|     ) -> Result<Vec<CalendarEvent>, String> { | ||||
|         let window = web_sys::window().ok_or("No global window exists")?; | ||||
|          | ||||
|         let opts = RequestInit::new(); | ||||
|         opts.set_method("GET"); | ||||
|         opts.set_mode(RequestMode::Cors); | ||||
|  | ||||
|         let url = format!("{}/calendar/events?year={}&month={}", self.base_url, year, month); | ||||
|         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))?; | ||||
|  | ||||
|         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() { | ||||
|             let events: Vec<CalendarEvent> = serde_json::from_str(&text_string) | ||||
|                 .map_err(|e| format!("JSON parsing failed: {}", e))?; | ||||
|             Ok(events) | ||||
|         } else { | ||||
|             Err(format!("Request failed with status {}: {}", resp.status(), text_string)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Convert events to a HashMap grouped by date for calendar display | ||||
|     pub fn group_events_by_date(events: Vec<CalendarEvent>) -> HashMap<NaiveDate, Vec<String>> { | ||||
|         let mut grouped = HashMap::new(); | ||||
|          | ||||
|         for event in events { | ||||
|             let date = event.get_date(); | ||||
|             let title = event.get_title(); | ||||
|              | ||||
|             grouped.entry(date) | ||||
|                 .or_insert_with(Vec::new) | ||||
|                 .push(title); | ||||
|         } | ||||
|          | ||||
|         grouped | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/services/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/services/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| pub mod calendar_service; | ||||
|  | ||||
| pub use calendar_service::CalendarService; | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone