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>
108 lines
3.4 KiB
Rust
108 lines
3.4 KiB
Rust
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
|
|
}
|
|
} |