Implement comprehensive recurring event support
- Add RRULE parsing for DAILY, WEEKLY, MONTHLY, YEARLY frequencies - Support INTERVAL, COUNT, and BYDAY recurrence parameters - Generate event occurrences across 30 days past to 365 days future - Update event refresh to regenerate all recurring occurrences - Clean up unused imports for cleaner compilation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										31
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								src/app.rs
									
									
									
									
									
								
							| @@ -138,16 +138,33 @@ fn CalendarView() -> Html { | ||||
|                      | ||||
|                     match calendar_service.refresh_event(&token, &uid).await { | ||||
|                         Ok(Some(refreshed_event)) => { | ||||
|                             // Update the event in the existing events map | ||||
|                             // If this is a recurring event, we need to regenerate all occurrences | ||||
|                             let mut updated_events = (*events).clone(); | ||||
|                              | ||||
|                             // First, remove all existing occurrences of this event | ||||
|                             for (_, day_events) in updated_events.iter_mut() { | ||||
|                                 for existing_event in day_events.iter_mut() { | ||||
|                                     if existing_event.uid == uid { | ||||
|                                         *existing_event = refreshed_event.clone(); | ||||
|                                         break; | ||||
|                                     } | ||||
|                                 } | ||||
|                                 day_events.retain(|e| e.uid != uid); | ||||
|                             } | ||||
|                              | ||||
|                             // Then, if it's a recurring event, generate new occurrences | ||||
|                             if refreshed_event.recurrence_rule.is_some() { | ||||
|                                 let new_occurrences = CalendarService::expand_recurring_events(vec![refreshed_event.clone()]); | ||||
|                                  | ||||
|                                 // Add all new occurrences to the appropriate dates | ||||
|                                 for occurrence in new_occurrences { | ||||
|                                     let date = occurrence.get_date(); | ||||
|                                     updated_events.entry(date) | ||||
|                                         .or_insert_with(Vec::new) | ||||
|                                         .push(occurrence); | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 // Non-recurring event, just add it to the appropriate date | ||||
|                                 let date = refreshed_event.get_date(); | ||||
|                                 updated_events.entry(date) | ||||
|                                     .or_insert_with(Vec::new) | ||||
|                                     .push(refreshed_event); | ||||
|                             } | ||||
|                              | ||||
|                             events.set(updated_events); | ||||
|                         } | ||||
|                         Ok(None) => { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| use chrono::{DateTime, Utc, NaiveDate}; | ||||
| use chrono::{DateTime, Utc, NaiveDate, Datelike, Weekday, Duration}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use wasm_bindgen::JsCast; | ||||
| use wasm_bindgen_futures::JsFuture; | ||||
| @@ -183,7 +183,10 @@ impl CalendarService { | ||||
|     pub fn group_events_by_date(events: Vec<CalendarEvent>) -> HashMap<NaiveDate, Vec<CalendarEvent>> { | ||||
|         let mut grouped = HashMap::new(); | ||||
|          | ||||
|         for event in events { | ||||
|         // Expand recurring events first | ||||
|         let expanded_events = Self::expand_recurring_events(events); | ||||
|          | ||||
|         for event in expanded_events { | ||||
|             let date = event.get_date(); | ||||
|              | ||||
|             grouped.entry(date) | ||||
| @@ -194,6 +197,215 @@ impl CalendarService { | ||||
|         grouped | ||||
|     } | ||||
|  | ||||
|     /// Expand recurring events to show on all occurrence dates within a reasonable range | ||||
|     pub fn expand_recurring_events(events: Vec<CalendarEvent>) -> Vec<CalendarEvent> { | ||||
|         let mut expanded_events = Vec::new(); | ||||
|         let today = chrono::Utc::now().date_naive(); | ||||
|         let start_range = today - Duration::days(30); // Show past 30 days | ||||
|         let end_range = today + Duration::days(365);   // Show next 365 days | ||||
|          | ||||
|         for event in events { | ||||
|             if let Some(ref rrule) = event.recurrence_rule { | ||||
|                 // Generate occurrences for recurring events | ||||
|                 let occurrences = Self::generate_occurrences(&event, rrule, start_range, end_range); | ||||
|                 expanded_events.extend(occurrences); | ||||
|             } else { | ||||
|                 // Non-recurring event - add as-is | ||||
|                 expanded_events.push(event); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         expanded_events | ||||
|     } | ||||
|  | ||||
|     /// Generate occurrence dates for a recurring event based on RRULE | ||||
|     fn generate_occurrences( | ||||
|         base_event: &CalendarEvent, | ||||
|         rrule: &str, | ||||
|         start_range: NaiveDate, | ||||
|         end_range: NaiveDate, | ||||
|     ) -> Vec<CalendarEvent> { | ||||
|         let mut occurrences = Vec::new(); | ||||
|          | ||||
|         // Parse RRULE components | ||||
|         let rrule_upper = rrule.to_uppercase(); | ||||
|         let components: HashMap<String, String> = rrule_upper | ||||
|             .split(';') | ||||
|             .filter_map(|part| { | ||||
|                 let mut split = part.split('='); | ||||
|                 if let (Some(key), Some(value)) = (split.next(), split.next()) { | ||||
|                     Some((key.to_string(), value.to_string())) | ||||
|                 } else { | ||||
|                     None | ||||
|                 } | ||||
|             }) | ||||
|             .collect(); | ||||
|  | ||||
|         // Get frequency | ||||
|         let freq = components.get("FREQ").map(|s| s.as_str()).unwrap_or("DAILY"); | ||||
|          | ||||
|         // Get interval (default 1) | ||||
|         let interval: i32 = components.get("INTERVAL") | ||||
|             .and_then(|s| s.parse().ok()) | ||||
|             .unwrap_or(1); | ||||
|  | ||||
|         // Get count limit (default 100 to prevent infinite loops) | ||||
|         let count: usize = components.get("COUNT") | ||||
|             .and_then(|s| s.parse().ok()) | ||||
|             .unwrap_or(100) | ||||
|             .min(365); // Cap at 365 occurrences for performance | ||||
|  | ||||
|         let start_date = base_event.start.date_naive(); | ||||
|         let mut current_date = start_date; | ||||
|         let mut occurrence_count = 0; | ||||
|  | ||||
|         // Generate occurrences based on frequency | ||||
|         while current_date <= end_range && occurrence_count < count { | ||||
|             if current_date >= start_range { | ||||
|                 // Create occurrence event | ||||
|                 let mut occurrence_event = base_event.clone(); | ||||
|                  | ||||
|                 // Adjust dates | ||||
|                 let days_diff = current_date.signed_duration_since(start_date).num_days(); | ||||
|                 occurrence_event.start = base_event.start + Duration::days(days_diff); | ||||
|                  | ||||
|                 if let Some(end) = base_event.end { | ||||
|                     occurrence_event.end = Some(end + Duration::days(days_diff)); | ||||
|                 } | ||||
|                  | ||||
|                 occurrences.push(occurrence_event); | ||||
|             } | ||||
|              | ||||
|             // Calculate next occurrence date | ||||
|             match freq { | ||||
|                 "DAILY" => { | ||||
|                     current_date = current_date + Duration::days(interval as i64); | ||||
|                 } | ||||
|                 "WEEKLY" => { | ||||
|                     if let Some(byday) = components.get("BYDAY") { | ||||
|                         // Handle specific days of week | ||||
|                         current_date = Self::next_weekday_occurrence(current_date, byday, interval); | ||||
|                     } else { | ||||
|                         current_date = current_date + Duration::weeks(interval as i64); | ||||
|                     } | ||||
|                 } | ||||
|                 "MONTHLY" => { | ||||
|                     // Simple monthly increment (same day of month) | ||||
|                     if let Some(next_month) = Self::add_months(current_date, interval) { | ||||
|                         current_date = next_month; | ||||
|                     } else { | ||||
|                         break; // Invalid date | ||||
|                     } | ||||
|                 } | ||||
|                 "YEARLY" => { | ||||
|                     if let Some(next_year) = Self::add_years(current_date, interval) { | ||||
|                         current_date = next_year; | ||||
|                     } else { | ||||
|                         break; // Invalid date | ||||
|                     } | ||||
|                 } | ||||
|                 _ => break, // Unsupported frequency | ||||
|             } | ||||
|              | ||||
|             occurrence_count += 1; | ||||
|         } | ||||
|          | ||||
|         occurrences | ||||
|     } | ||||
|  | ||||
|     /// Calculate next weekday occurrence for WEEKLY frequency with BYDAY | ||||
|     fn next_weekday_occurrence(current_date: NaiveDate, byday: &str, interval: i32) -> NaiveDate { | ||||
|         let weekdays = Self::parse_byday(byday); | ||||
|         if weekdays.is_empty() { | ||||
|             return current_date + Duration::weeks(interval as i64); | ||||
|         } | ||||
|  | ||||
|         let current_weekday = current_date.weekday(); | ||||
|          | ||||
|         // Find next occurrence within current week | ||||
|         for &target_weekday in &weekdays { | ||||
|             let days_until = Self::days_until_weekday(current_weekday, target_weekday); | ||||
|             if days_until > 0 { | ||||
|                 return current_date + Duration::days(days_until as i64); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // No more occurrences this week, move to next interval | ||||
|         let next_week_start = current_date + Duration::weeks(interval as i64) - Duration::days(current_weekday.num_days_from_monday() as i64); | ||||
|         next_week_start + Duration::days(weekdays[0].num_days_from_monday() as i64) | ||||
|     } | ||||
|  | ||||
|     /// Parse BYDAY parameter (e.g., "MO,WE,FR" -> [Monday, Wednesday, Friday]) | ||||
|     fn parse_byday(byday: &str) -> Vec<Weekday> { | ||||
|         byday | ||||
|             .split(',') | ||||
|             .filter_map(|day| match day { | ||||
|                 "MO" => Some(Weekday::Mon), | ||||
|                 "TU" => Some(Weekday::Tue), | ||||
|                 "WE" => Some(Weekday::Wed), | ||||
|                 "TH" => Some(Weekday::Thu), | ||||
|                 "FR" => Some(Weekday::Fri), | ||||
|                 "SA" => Some(Weekday::Sat), | ||||
|                 "SU" => Some(Weekday::Sun), | ||||
|                 _ => None, | ||||
|             }) | ||||
|             .collect() | ||||
|     } | ||||
|  | ||||
|     /// Calculate days until target weekday | ||||
|     fn days_until_weekday(from: Weekday, to: Weekday) -> i32 { | ||||
|         let from_num = from.num_days_from_monday(); | ||||
|         let to_num = to.num_days_from_monday(); | ||||
|          | ||||
|         if to_num > from_num { | ||||
|             (to_num - from_num) as i32 | ||||
|         } else if to_num < from_num { | ||||
|             (7 + to_num - from_num) as i32 | ||||
|         } else { | ||||
|             0 // Same day | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Add months to a date (handling month boundary issues) | ||||
|     fn add_months(date: NaiveDate, months: i32) -> Option<NaiveDate> { | ||||
|         let mut year = date.year(); | ||||
|         let mut month = date.month() as i32 + months; | ||||
|          | ||||
|         while month > 12 { | ||||
|             year += 1; | ||||
|             month -= 12; | ||||
|         } | ||||
|         while month < 1 { | ||||
|             year -= 1; | ||||
|             month += 12; | ||||
|         } | ||||
|          | ||||
|         // Handle day overflow (e.g., Jan 31 -> Feb 28) | ||||
|         let day = date.day().min(Self::days_in_month(year, month as u32)); | ||||
|          | ||||
|         NaiveDate::from_ymd_opt(year, month as u32, day) | ||||
|     } | ||||
|  | ||||
|     /// Add years to a date | ||||
|     fn add_years(date: NaiveDate, years: i32) -> Option<NaiveDate> { | ||||
|         NaiveDate::from_ymd_opt(date.year() + years, date.month(), date.day()) | ||||
|     } | ||||
|  | ||||
|     /// Get number of days in a month | ||||
|     fn days_in_month(year: i32, month: u32) -> u32 { | ||||
|         match month { | ||||
|             1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, | ||||
|             4 | 6 | 9 | 11 => 30, | ||||
|             2 => if Self::is_leap_year(year) { 29 } else { 28 }, | ||||
|             _ => 30, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Check if year is leap year | ||||
|     fn is_leap_year(year: i32) -> bool { | ||||
|         (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) | ||||
|     } | ||||
|  | ||||
|     /// Refresh a single event by UID from the CalDAV server | ||||
|     pub async fn refresh_event(&self, token: &str, uid: &str) -> Result<Option<CalendarEvent>, String> { | ||||
|         let window = web_sys::window().ok_or("No global window exists")?; | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| pub mod calendar_service; | ||||
|  | ||||
| pub use calendar_service::{CalendarService, CalendarEvent, EventStatus, EventClass, EventReminder, ReminderAction}; | ||||
| pub use calendar_service::{CalendarService, CalendarEvent, EventReminder, ReminderAction}; | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone