Add comprehensive iCal properties support to event creation modal
Enhanced the create event modal to include all major iCalendar properties: - Event status (confirmed/tentative/cancelled) - Privacy classification (public/private/confidential) - Priority levels (0-9 numeric scale) - Organizer email field - Attendees list (comma-separated emails) - Categories (comma-separated tags) - Reminder options (none to 1 week before) - Recurrence patterns (none/daily/weekly/monthly/yearly) Updated backend to parse and handle all new fields, with proper enum conversion and comma-separated list parsing. Events now generate complete iCal data with STATUS, CLASS, PRIORITY, ORGANIZER, ATTENDEE, CATEGORIES, VALARM, and RRULE properties. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										42
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								src/app.rs
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ use yew::prelude::*; | ||||
| use yew_router::prelude::*; | ||||
| use gloo_storage::{LocalStorage, Storage}; | ||||
| use web_sys::MouseEvent; | ||||
| use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler}; | ||||
| use crate::components::{Sidebar, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType}; | ||||
| use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}}; | ||||
| use chrono::NaiveDate; | ||||
|  | ||||
| @@ -221,6 +221,38 @@ pub fn App() -> Html { | ||||
|                     let end_date = event_data.end_date.format("%Y-%m-%d").to_string(); | ||||
|                     let end_time = event_data.end_time.format("%H:%M").to_string(); | ||||
|                      | ||||
|                     // Convert enums to strings for backend | ||||
|                     let status_str = match event_data.status { | ||||
|                         EventStatus::Tentative => "tentative", | ||||
|                         EventStatus::Cancelled => "cancelled", | ||||
|                         _ => "confirmed", | ||||
|                     }.to_string(); | ||||
|                      | ||||
|                     let class_str = match event_data.class { | ||||
|                         EventClass::Private => "private", | ||||
|                         EventClass::Confidential => "confidential", | ||||
|                         _ => "public", | ||||
|                     }.to_string(); | ||||
|                      | ||||
|                     let reminder_str = match event_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 event_data.recurrence { | ||||
|                         RecurrenceType::Daily => "daily", | ||||
|                         RecurrenceType::Weekly => "weekly", | ||||
|                         RecurrenceType::Monthly => "monthly", | ||||
|                         RecurrenceType::Yearly => "yearly", | ||||
|                         _ => "none", | ||||
|                     }.to_string(); | ||||
|  | ||||
|                     match calendar_service.create_event( | ||||
|                         &token, | ||||
|                         &password, | ||||
| @@ -232,6 +264,14 @@ pub fn App() -> Html { | ||||
|                         end_time, | ||||
|                         event_data.location, | ||||
|                         event_data.all_day, | ||||
|                         status_str, | ||||
|                         class_str, | ||||
|                         event_data.priority, | ||||
|                         event_data.organizer, | ||||
|                         event_data.attendees, | ||||
|                         event_data.categories, | ||||
|                         reminder_str, | ||||
|                         recurrence_str, | ||||
|                         None // Let backend use first available calendar | ||||
|                     ).await { | ||||
|                         Ok(_) => { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| use yew::prelude::*; | ||||
| use web_sys::{HtmlInputElement, HtmlTextAreaElement}; | ||||
| use web_sys::{HtmlInputElement, HtmlTextAreaElement, HtmlSelectElement}; | ||||
| use chrono::{NaiveDate, NaiveTime}; | ||||
|  | ||||
| #[derive(Properties, PartialEq)] | ||||
| @@ -10,6 +10,65 @@ pub struct CreateEventModalProps { | ||||
|     pub on_create: Callback<EventCreationData>, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, PartialEq, Debug)] | ||||
| pub enum EventStatus { | ||||
|     Tentative, | ||||
|     Confirmed, | ||||
|     Cancelled, | ||||
| } | ||||
|  | ||||
| impl Default for EventStatus { | ||||
|     fn default() -> Self { | ||||
|         EventStatus::Confirmed | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, PartialEq, Debug)] | ||||
| pub enum EventClass { | ||||
|     Public, | ||||
|     Private, | ||||
|     Confidential, | ||||
| } | ||||
|  | ||||
| impl Default for EventClass { | ||||
|     fn default() -> Self { | ||||
|         EventClass::Public | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, PartialEq, Debug)] | ||||
| pub enum ReminderType { | ||||
|     None, | ||||
|     Minutes15, | ||||
|     Minutes30, | ||||
|     Hour1, | ||||
|     Hours2, | ||||
|     Day1, | ||||
|     Days2, | ||||
|     Week1, | ||||
| } | ||||
|  | ||||
| impl Default for ReminderType { | ||||
|     fn default() -> Self { | ||||
|         ReminderType::None | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, PartialEq, Debug)] | ||||
| pub enum RecurrenceType { | ||||
|     None, | ||||
|     Daily, | ||||
|     Weekly, | ||||
|     Monthly, | ||||
|     Yearly, | ||||
| } | ||||
|  | ||||
| impl Default for RecurrenceType { | ||||
|     fn default() -> Self { | ||||
|         RecurrenceType::None | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, PartialEq, Debug)] | ||||
| pub struct EventCreationData { | ||||
|     pub title: String, | ||||
| @@ -20,6 +79,14 @@ pub struct EventCreationData { | ||||
|     pub end_time: NaiveTime, | ||||
|     pub location: String, | ||||
|     pub all_day: bool, | ||||
|     pub status: EventStatus, | ||||
|     pub class: EventClass, | ||||
|     pub priority: Option<u8>, | ||||
|     pub organizer: String, | ||||
|     pub attendees: String, // Comma-separated list | ||||
|     pub categories: String, // Comma-separated list | ||||
|     pub reminder: ReminderType, | ||||
|     pub recurrence: RecurrenceType, | ||||
| } | ||||
|  | ||||
| impl Default for EventCreationData { | ||||
| @@ -37,6 +104,14 @@ impl Default for EventCreationData { | ||||
|             end_time, | ||||
|             location: String::new(), | ||||
|             all_day: false, | ||||
|             status: EventStatus::default(), | ||||
|             class: EventClass::default(), | ||||
|             priority: None, | ||||
|             organizer: String::new(), | ||||
|             attendees: String::new(), | ||||
|             categories: String::new(), | ||||
|             reminder: ReminderType::default(), | ||||
|             recurrence: RecurrenceType::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -109,6 +184,117 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_organizer_input = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: InputEvent| { | ||||
|             if let Some(input) = e.target_dyn_into::<HtmlInputElement>() { | ||||
|                 let mut data = (*event_data).clone(); | ||||
|                 data.organizer = input.value(); | ||||
|                 event_data.set(data); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_attendees_input = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: InputEvent| { | ||||
|             if let Some(textarea) = e.target_dyn_into::<HtmlTextAreaElement>() { | ||||
|                 let mut data = (*event_data).clone(); | ||||
|                 data.attendees = textarea.value(); | ||||
|                 event_data.set(data); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_categories_input = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: InputEvent| { | ||||
|             if let Some(input) = e.target_dyn_into::<HtmlInputElement>() { | ||||
|                 let mut data = (*event_data).clone(); | ||||
|                 data.categories = input.value(); | ||||
|                 event_data.set(data); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_status_change = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: Event| { | ||||
|             if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() { | ||||
|                 let mut data = (*event_data).clone(); | ||||
|                 data.status = match select.value().as_str() { | ||||
|                     "tentative" => EventStatus::Tentative, | ||||
|                     "cancelled" => EventStatus::Cancelled, | ||||
|                     _ => EventStatus::Confirmed, | ||||
|                 }; | ||||
|                 event_data.set(data); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_class_change = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: Event| { | ||||
|             if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() { | ||||
|                 let mut data = (*event_data).clone(); | ||||
|                 data.class = match select.value().as_str() { | ||||
|                     "private" => EventClass::Private, | ||||
|                     "confidential" => EventClass::Confidential, | ||||
|                     _ => EventClass::Public, | ||||
|                 }; | ||||
|                 event_data.set(data); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_priority_input = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: InputEvent| { | ||||
|             if let Some(input) = e.target_dyn_into::<HtmlInputElement>() { | ||||
|                 let mut data = (*event_data).clone(); | ||||
|                 data.priority = input.value().parse::<u8>().ok().filter(|&p| p <= 9); | ||||
|                 event_data.set(data); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_reminder_change = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: Event| { | ||||
|             if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() { | ||||
|                 let mut data = (*event_data).clone(); | ||||
|                 data.reminder = match select.value().as_str() { | ||||
|                     "15min" => ReminderType::Minutes15, | ||||
|                     "30min" => ReminderType::Minutes30, | ||||
|                     "1hour" => ReminderType::Hour1, | ||||
|                     "2hours" => ReminderType::Hours2, | ||||
|                     "1day" => ReminderType::Day1, | ||||
|                     "2days" => ReminderType::Days2, | ||||
|                     "1week" => ReminderType::Week1, | ||||
|                     _ => ReminderType::None, | ||||
|                 }; | ||||
|                 event_data.set(data); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_recurrence_change = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: Event| { | ||||
|             if let Some(select) = e.target_dyn_into::<HtmlSelectElement>() { | ||||
|                 let mut data = (*event_data).clone(); | ||||
|                 data.recurrence = match select.value().as_str() { | ||||
|                     "daily" => RecurrenceType::Daily, | ||||
|                     "weekly" => RecurrenceType::Weekly, | ||||
|                     "monthly" => RecurrenceType::Monthly, | ||||
|                     "yearly" => RecurrenceType::Yearly, | ||||
|                     _ => RecurrenceType::None, | ||||
|                 }; | ||||
|                 event_data.set(data); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_start_date_change = { | ||||
|         let event_data = event_data.clone(); | ||||
|         Callback::from(move |e: Event| { | ||||
| @@ -302,6 +488,119 @@ pub fn create_event_modal(props: &CreateEventModalProps) -> Html { | ||||
|                             placeholder="Enter event location" | ||||
|                         /> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-row"> | ||||
|                         <div class="form-group"> | ||||
|                             <label for="event-status">{"Status"}</label> | ||||
|                             <select  | ||||
|                                 id="event-status" | ||||
|                                 class="form-input"  | ||||
|                                 onchange={on_status_change} | ||||
|                             > | ||||
|                                 <option value="confirmed" selected={matches!(data.status, EventStatus::Confirmed)}>{"Confirmed"}</option> | ||||
|                                 <option value="tentative" selected={matches!(data.status, EventStatus::Tentative)}>{"Tentative"}</option> | ||||
|                                 <option value="cancelled" selected={matches!(data.status, EventStatus::Cancelled)}>{"Cancelled"}</option> | ||||
|                             </select> | ||||
|                         </div> | ||||
|                          | ||||
|                         <div class="form-group"> | ||||
|                             <label for="event-class">{"Privacy"}</label> | ||||
|                             <select  | ||||
|                                 id="event-class" | ||||
|                                 class="form-input"  | ||||
|                                 onchange={on_class_change} | ||||
|                             > | ||||
|                                 <option value="public" selected={matches!(data.class, EventClass::Public)}>{"Public"}</option> | ||||
|                                 <option value="private" selected={matches!(data.class, EventClass::Private)}>{"Private"}</option> | ||||
|                                 <option value="confidential" selected={matches!(data.class, EventClass::Confidential)}>{"Confidential"}</option> | ||||
|                             </select> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-group"> | ||||
|                         <label for="event-priority">{"Priority (0-9, optional)"}</label> | ||||
|                         <input  | ||||
|                             type="number"  | ||||
|                             id="event-priority" | ||||
|                             class="form-input"  | ||||
|                             value={data.priority.map(|p| p.to_string()).unwrap_or_default()} | ||||
|                             oninput={on_priority_input} | ||||
|                             placeholder="0-9 priority level" | ||||
|                             min="0" | ||||
|                             max="9" | ||||
|                         /> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-group"> | ||||
|                         <label for="event-organizer">{"Organizer Email"}</label> | ||||
|                         <input  | ||||
|                             type="email"  | ||||
|                             id="event-organizer" | ||||
|                             class="form-input"  | ||||
|                             value={data.organizer.clone()} | ||||
|                             oninput={on_organizer_input} | ||||
|                             placeholder="organizer@example.com" | ||||
|                         /> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-group"> | ||||
|                         <label for="event-attendees">{"Attendees (comma-separated emails)"}</label> | ||||
|                         <textarea  | ||||
|                             id="event-attendees" | ||||
|                             class="form-input"  | ||||
|                             value={data.attendees.clone()} | ||||
|                             oninput={on_attendees_input} | ||||
|                             placeholder="attendee1@example.com, attendee2@example.com" | ||||
|                             rows="2" | ||||
|                         ></textarea> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-group"> | ||||
|                         <label for="event-categories">{"Categories (comma-separated)"}</label> | ||||
|                         <input  | ||||
|                             type="text"  | ||||
|                             id="event-categories" | ||||
|                             class="form-input"  | ||||
|                             value={data.categories.clone()} | ||||
|                             oninput={on_categories_input} | ||||
|                             placeholder="work, meeting, personal" | ||||
|                         /> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-row"> | ||||
|                         <div class="form-group"> | ||||
|                             <label for="event-reminder">{"Reminder"}</label> | ||||
|                             <select  | ||||
|                                 id="event-reminder" | ||||
|                                 class="form-input"  | ||||
|                                 onchange={on_reminder_change} | ||||
|                             > | ||||
|                                 <option value="none" selected={matches!(data.reminder, ReminderType::None)}>{"None"}</option> | ||||
|                                 <option value="15min" selected={matches!(data.reminder, ReminderType::Minutes15)}>{"15 minutes"}</option> | ||||
|                                 <option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes"}</option> | ||||
|                                 <option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour"}</option> | ||||
|                                 <option value="2hours" selected={matches!(data.reminder, ReminderType::Hours2)}>{"2 hours"}</option> | ||||
|                                 <option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day"}</option> | ||||
|                                 <option value="2days" selected={matches!(data.reminder, ReminderType::Days2)}>{"2 days"}</option> | ||||
|                                 <option value="1week" selected={matches!(data.reminder, ReminderType::Week1)}>{"1 week"}</option> | ||||
|                             </select> | ||||
|                         </div> | ||||
|                          | ||||
|                         <div class="form-group"> | ||||
|                             <label for="event-recurrence">{"Recurrence"}</label> | ||||
|                             <select  | ||||
|                                 id="event-recurrence" | ||||
|                                 class="form-input"  | ||||
|                                 onchange={on_recurrence_change} | ||||
|                             > | ||||
|                                 <option value="none" selected={matches!(data.recurrence, RecurrenceType::None)}>{"None"}</option> | ||||
|                                 <option value="daily" selected={matches!(data.recurrence, RecurrenceType::Daily)}>{"Daily"}</option> | ||||
|                                 <option value="weekly" selected={matches!(data.recurrence, RecurrenceType::Weekly)}>{"Weekly"}</option> | ||||
|                                 <option value="monthly" selected={matches!(data.recurrence, RecurrenceType::Monthly)}>{"Monthly"}</option> | ||||
|                                 <option value="yearly" selected={matches!(data.recurrence, RecurrenceType::Yearly)}>{"Yearly"}</option> | ||||
|                             </select> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                  | ||||
|                 <div class="modal-footer"> | ||||
|   | ||||
| @@ -17,7 +17,7 @@ pub use create_calendar_modal::CreateCalendarModal; | ||||
| pub use context_menu::ContextMenu; | ||||
| pub use event_context_menu::EventContextMenu; | ||||
| pub use calendar_context_menu::CalendarContextMenu; | ||||
| pub use create_event_modal::{CreateEventModal, EventCreationData}; | ||||
| pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType}; | ||||
| pub use sidebar::Sidebar; | ||||
| pub use calendar_list_item::CalendarListItem; | ||||
| pub use route_handler::RouteHandler; | ||||
| @@ -599,6 +599,14 @@ impl CalendarService { | ||||
|         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, | ||||
|         calendar_path: Option<String> | ||||
|     ) -> Result<(), String> { | ||||
|         let window = web_sys::window().ok_or("No global window exists")?; | ||||
| @@ -616,6 +624,14 @@ impl CalendarService { | ||||
|             "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, | ||||
|             "calendar_path": calendar_path | ||||
|         }); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone