Implement shared RFC 5545 VEvent library with workspace restructuring
- Created calendar-models/ shared library with RFC 5545-compliant VEvent structures - Migrated backend to use shared VEvent with proper field mappings (dtstart/dtend, rrule, exdate, etc.) - Converted CalDAV client to parse into VEvent structures with structured types - Updated all CRUD handlers to use VEvent with CalendarUser, Attendee, VAlarm types - Restructured project as Cargo workspace with frontend/, backend/, calendar-models/ - Updated Trunk configuration for new directory structure - Fixed all compilation errors and field references throughout codebase - Updated documentation and build instructions for workspace structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										316
									
								
								frontend/src/components/calendar.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								frontend/src/components/calendar.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,316 @@ | ||||
| use yew::prelude::*; | ||||
| use chrono::{Datelike, Local, NaiveDate, Duration}; | ||||
| use std::collections::HashMap; | ||||
| use web_sys::MouseEvent; | ||||
| use crate::services::calendar_service::UserInfo; | ||||
| use crate::models::ical::VEvent; | ||||
| use crate::components::{EventModal, ViewMode, CalendarHeader, MonthView, WeekView, CreateEventModal, EventCreationData}; | ||||
| use gloo_storage::{LocalStorage, Storage}; | ||||
|  | ||||
| #[derive(Properties, PartialEq)] | ||||
| pub struct CalendarProps { | ||||
|     #[prop_or_default] | ||||
|     pub events: HashMap<NaiveDate, Vec<VEvent>>, | ||||
|     pub on_event_click: Callback<VEvent>, | ||||
|     #[prop_or_default] | ||||
|     pub refreshing_event_uid: Option<String>, | ||||
|     #[prop_or_default] | ||||
|     pub user_info: Option<UserInfo>, | ||||
|     #[prop_or_default] | ||||
|     pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, VEvent)>>, | ||||
|     #[prop_or_default] | ||||
|     pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>, | ||||
|     #[prop_or_default] | ||||
|     pub view: ViewMode, | ||||
|     #[prop_or_default] | ||||
|     pub on_create_event_request: Option<Callback<EventCreationData>>, | ||||
|     #[prop_or_default] | ||||
|     pub on_event_update_request: Option<Callback<(VEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>, | ||||
|     #[prop_or_default] | ||||
|     pub context_menus_open: bool, | ||||
| } | ||||
|  | ||||
| #[function_component] | ||||
| pub fn Calendar(props: &CalendarProps) -> Html { | ||||
|     let today = Local::now().date_naive(); | ||||
|     // Track the currently selected date (the actual day the user has selected) | ||||
|     let selected_date = use_state(|| { | ||||
|         // Try to load saved selected date from localStorage | ||||
|         if let Ok(saved_date_str) = LocalStorage::get::<String>("calendar_selected_date") { | ||||
|             if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { | ||||
|                 saved_date | ||||
|             } else { | ||||
|                 today | ||||
|             } | ||||
|         } else { | ||||
|             // Check for old key for backward compatibility | ||||
|             if let Ok(saved_date_str) = LocalStorage::get::<String>("calendar_current_month") { | ||||
|                 if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { | ||||
|                     saved_date | ||||
|                 } else { | ||||
|                     today | ||||
|                 } | ||||
|             } else { | ||||
|                 today | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     // Track the display date (what to show in the view) | ||||
|     let current_date = use_state(|| { | ||||
|         match props.view { | ||||
|             ViewMode::Month => selected_date.with_day(1).unwrap_or(*selected_date), | ||||
|             ViewMode::Week => *selected_date, | ||||
|         } | ||||
|     }); | ||||
|     let selected_event = use_state(|| None::<VEvent>); | ||||
|      | ||||
|     // State for create event modal | ||||
|     let show_create_modal = use_state(|| false); | ||||
|     let create_event_data = use_state(|| None::<(chrono::NaiveDate, chrono::NaiveTime, chrono::NaiveTime)>); | ||||
|      | ||||
|     // State for time increment snapping (15 or 30 minutes) | ||||
|     let time_increment = use_state(|| { | ||||
|         // Try to load saved time increment from localStorage | ||||
|         if let Ok(saved_increment) = LocalStorage::get::<u32>("calendar_time_increment") { | ||||
|             if saved_increment == 15 || saved_increment == 30 { | ||||
|                 saved_increment | ||||
|             } else { | ||||
|                 15 | ||||
|             } | ||||
|         } else { | ||||
|             15 | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     // Handle view mode changes - adjust current_date format when switching between month/week | ||||
|     { | ||||
|         let current_date = current_date.clone(); | ||||
|         let selected_date = selected_date.clone(); | ||||
|         let view = props.view.clone(); | ||||
|         use_effect_with(view, move |view_mode| { | ||||
|             let selected = *selected_date; | ||||
|             let new_display_date = match view_mode { | ||||
|                 ViewMode::Month => selected.with_day(1).unwrap_or(selected), | ||||
|                 ViewMode::Week => selected, // Show the week containing the selected date | ||||
|             }; | ||||
|             current_date.set(new_display_date); | ||||
|             || {} | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     let on_prev = { | ||||
|         let current_date = current_date.clone(); | ||||
|         let selected_date = selected_date.clone(); | ||||
|         let view = props.view.clone(); | ||||
|         Callback::from(move |_: MouseEvent| { | ||||
|             let (new_selected, new_display) = match view { | ||||
|                 ViewMode::Month => { | ||||
|                     // Go to previous month, select the 1st day | ||||
|                     let prev_month = *current_date - Duration::days(1); | ||||
|                     let first_of_prev = prev_month.with_day(1).unwrap(); | ||||
|                     (first_of_prev, first_of_prev) | ||||
|                 }, | ||||
|                 ViewMode::Week => { | ||||
|                     // Go to previous week | ||||
|                     let prev_week = *selected_date - Duration::weeks(1); | ||||
|                     (prev_week, prev_week) | ||||
|                 }, | ||||
|             }; | ||||
|             selected_date.set(new_selected); | ||||
|             current_date.set(new_display); | ||||
|             let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); | ||||
|         }) | ||||
|     }; | ||||
|      | ||||
|     let on_next = { | ||||
|         let current_date = current_date.clone(); | ||||
|         let selected_date = selected_date.clone(); | ||||
|         let view = props.view.clone(); | ||||
|         Callback::from(move |_: MouseEvent| { | ||||
|             let (new_selected, new_display) = match view { | ||||
|                 ViewMode::Month => { | ||||
|                     // Go to next month, select the 1st day | ||||
|                     let next_month = if current_date.month() == 12 { | ||||
|                         NaiveDate::from_ymd_opt(current_date.year() + 1, 1, 1).unwrap() | ||||
|                     } else { | ||||
|                         NaiveDate::from_ymd_opt(current_date.year(), current_date.month() + 1, 1).unwrap() | ||||
|                     }; | ||||
|                     (next_month, next_month) | ||||
|                 }, | ||||
|                 ViewMode::Week => { | ||||
|                     // Go to next week | ||||
|                     let next_week = *selected_date + Duration::weeks(1); | ||||
|                     (next_week, next_week) | ||||
|                 }, | ||||
|             }; | ||||
|             selected_date.set(new_selected); | ||||
|             current_date.set(new_display); | ||||
|             let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_today = { | ||||
|         let current_date = current_date.clone(); | ||||
|         let selected_date = selected_date.clone(); | ||||
|         let view = props.view.clone(); | ||||
|         Callback::from(move |_| { | ||||
|             let today = Local::now().date_naive(); | ||||
|             let (new_selected, new_display) = match view { | ||||
|                 ViewMode::Month => { | ||||
|                     let first_of_today = today.with_day(1).unwrap(); | ||||
|                     (today, first_of_today) // Select today, but display the month | ||||
|                 }, | ||||
|                 ViewMode::Week => (today, today), // Select and display today | ||||
|             }; | ||||
|             selected_date.set(new_selected); | ||||
|             current_date.set(new_display); | ||||
|             let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); | ||||
|         }) | ||||
|     }; | ||||
|      | ||||
|     // Handle time increment toggle | ||||
|     let on_time_increment_toggle = { | ||||
|         let time_increment = time_increment.clone(); | ||||
|         Callback::from(move |_: MouseEvent| { | ||||
|             let current = *time_increment; | ||||
|             let next = if current == 15 { 30 } else { 15 }; | ||||
|             time_increment.set(next); | ||||
|             let _ = LocalStorage::set("calendar_time_increment", next); | ||||
|         }) | ||||
|     }; | ||||
|      | ||||
|     // Handle drag-to-create event | ||||
|     let on_create_event = { | ||||
|         let show_create_modal = show_create_modal.clone(); | ||||
|         let create_event_data = create_event_data.clone(); | ||||
|         Callback::from(move |(_date, start_datetime, end_datetime): (NaiveDate, chrono::NaiveDateTime, chrono::NaiveDateTime)| { | ||||
|             // For drag-to-create, we don't need the temporary event approach | ||||
|             // Instead, just pass the local times directly via initial_time props | ||||
|             create_event_data.set(Some((start_datetime.date(), start_datetime.time(), end_datetime.time()))); | ||||
|             show_create_modal.set(true); | ||||
|         }) | ||||
|     }; | ||||
|      | ||||
|     // Handle drag-to-move event | ||||
|     let on_event_update = { | ||||
|         let on_event_update_request = props.on_event_update_request.clone(); | ||||
|         Callback::from(move |(event, new_start, new_end, preserve_rrule, until_date): (VEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)| { | ||||
|             if let Some(callback) = &on_event_update_request { | ||||
|                 callback.emit((event, new_start, new_end, preserve_rrule, until_date)); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     html! { | ||||
|         <div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}> | ||||
|             <CalendarHeader  | ||||
|                 current_date={*current_date} | ||||
|                 view_mode={props.view.clone()} | ||||
|                 on_prev={on_prev} | ||||
|                 on_next={on_next} | ||||
|                 on_today={on_today} | ||||
|                 time_increment={Some(*time_increment)} | ||||
|                 on_time_increment_toggle={Some(on_time_increment_toggle)} | ||||
|             /> | ||||
|              | ||||
|             { | ||||
|                 match props.view { | ||||
|                     ViewMode::Month => { | ||||
|                         let on_day_select = { | ||||
|                             let selected_date = selected_date.clone(); | ||||
|                             Callback::from(move |date: NaiveDate| { | ||||
|                                 selected_date.set(date); | ||||
|                                 let _ = LocalStorage::set("calendar_selected_date", date.format("%Y-%m-%d").to_string()); | ||||
|                             }) | ||||
|                         }; | ||||
|                          | ||||
|                         html! { | ||||
|                             <MonthView | ||||
|                                 current_month={*current_date} | ||||
|                                 today={today} | ||||
|                                 events={props.events.clone()} | ||||
|                                 on_event_click={props.on_event_click.clone()} | ||||
|                                 refreshing_event_uid={props.refreshing_event_uid.clone()} | ||||
|                                 user_info={props.user_info.clone()} | ||||
|                                 on_event_context_menu={props.on_event_context_menu.clone()} | ||||
|                                 on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||
|                                 selected_date={Some(*selected_date)} | ||||
|                                 on_day_select={Some(on_day_select)} | ||||
|                             /> | ||||
|                         } | ||||
|                     }, | ||||
|                     ViewMode::Week => html! { | ||||
|                         <WeekView | ||||
|                             current_date={*current_date} | ||||
|                             today={today} | ||||
|                             events={props.events.clone()} | ||||
|                             on_event_click={props.on_event_click.clone()} | ||||
|                             refreshing_event_uid={props.refreshing_event_uid.clone()} | ||||
|                             user_info={props.user_info.clone()} | ||||
|                             on_event_context_menu={props.on_event_context_menu.clone()} | ||||
|                             on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||
|                             on_create_event={Some(on_create_event)} | ||||
|                             on_create_event_request={props.on_create_event_request.clone()} | ||||
|                             on_event_update={Some(on_event_update)} | ||||
|                             context_menus_open={props.context_menus_open} | ||||
|                             time_increment={*time_increment} | ||||
|                         /> | ||||
|                     }, | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             // Event details modal | ||||
|             <EventModal  | ||||
|                 event={(*selected_event).clone()} | ||||
|                 on_close={{ | ||||
|                     let selected_event_clone = selected_event.clone(); | ||||
|                     Callback::from(move |_| { | ||||
|                         selected_event_clone.set(None); | ||||
|                     }) | ||||
|                 }} | ||||
|             /> | ||||
|              | ||||
|             // Create event modal | ||||
|             <CreateEventModal | ||||
|                 is_open={*show_create_modal} | ||||
|                 selected_date={create_event_data.as_ref().map(|(date, _, _)| *date)} | ||||
|                 event_to_edit={None} | ||||
|                 available_calendars={props.user_info.as_ref().map(|info| info.calendars.clone()).unwrap_or_default()} | ||||
|                 initial_start_time={create_event_data.as_ref().map(|(_, start_time, _)| *start_time)} | ||||
|                 initial_end_time={create_event_data.as_ref().map(|(_, _, end_time)| *end_time)} | ||||
|                 on_close={{ | ||||
|                     let show_create_modal = show_create_modal.clone(); | ||||
|                     let create_event_data = create_event_data.clone(); | ||||
|                     Callback::from(move |_| { | ||||
|                         show_create_modal.set(false); | ||||
|                         create_event_data.set(None); | ||||
|                     }) | ||||
|                 }} | ||||
|                 on_create={{ | ||||
|                     let show_create_modal = show_create_modal.clone(); | ||||
|                     let create_event_data = create_event_data.clone(); | ||||
|                     let on_create_event_request = props.on_create_event_request.clone(); | ||||
|                     Callback::from(move |event_data: EventCreationData| { | ||||
|                         show_create_modal.set(false); | ||||
|                         create_event_data.set(None); | ||||
|                          | ||||
|                         // Emit the create event request to parent | ||||
|                         if let Some(callback) = &on_create_event_request { | ||||
|                             callback.emit(event_data); | ||||
|                         } | ||||
|                     }) | ||||
|                 }} | ||||
|                 on_update={{ | ||||
|                     let show_create_modal = show_create_modal.clone(); | ||||
|                     let create_event_data = create_event_data.clone(); | ||||
|                     Callback::from(move |(_original_event, _updated_data): (VEvent, EventCreationData)| { | ||||
|                         show_create_modal.set(false); | ||||
|                         create_event_data.set(None); | ||||
|                         // TODO: Handle actual event update | ||||
|                     }) | ||||
|                 }} | ||||
|             /> | ||||
|         </div> | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone