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:
		
							
								
								
									
										196
									
								
								frontend/src/components/create_calendar_modal.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								frontend/src/components/create_calendar_modal.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | ||||
| use yew::prelude::*; | ||||
|  | ||||
| #[derive(Properties, PartialEq)] | ||||
| pub struct CreateCalendarModalProps { | ||||
|     pub is_open: bool, | ||||
|     pub on_close: Callback<()>, | ||||
|     pub on_create: Callback<(String, Option<String>, Option<String>)>, // name, description, color | ||||
|     pub available_colors: Vec<String>, | ||||
| } | ||||
|  | ||||
| #[function_component] | ||||
| pub fn CreateCalendarModal(props: &CreateCalendarModalProps) -> Html { | ||||
|     let calendar_name = use_state(|| String::new()); | ||||
|     let description = use_state(|| String::new()); | ||||
|     let selected_color = use_state(|| None::<String>); | ||||
|     let error_message = use_state(|| None::<String>); | ||||
|     let is_creating = use_state(|| false); | ||||
|  | ||||
|     let on_name_change = { | ||||
|         let calendar_name = calendar_name.clone(); | ||||
|         Callback::from(move |e: InputEvent| { | ||||
|             let input: web_sys::HtmlInputElement = e.target_unchecked_into(); | ||||
|             calendar_name.set(input.value()); | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_description_change = { | ||||
|         let description = description.clone(); | ||||
|         Callback::from(move |e: InputEvent| { | ||||
|             let input: web_sys::HtmlTextAreaElement = e.target_unchecked_into(); | ||||
|             description.set(input.value()); | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_submit = { | ||||
|         let calendar_name = calendar_name.clone(); | ||||
|         let description = description.clone(); | ||||
|         let selected_color = selected_color.clone(); | ||||
|         let error_message = error_message.clone(); | ||||
|         let is_creating = is_creating.clone(); | ||||
|         let on_create = props.on_create.clone(); | ||||
|          | ||||
|         Callback::from(move |e: SubmitEvent| { | ||||
|             e.prevent_default(); | ||||
|              | ||||
|             let name = (*calendar_name).trim(); | ||||
|             if name.is_empty() { | ||||
|                 error_message.set(Some("Calendar name is required".to_string())); | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             if name.len() > 100 { | ||||
|                 error_message.set(Some("Calendar name too long (max 100 characters)".to_string())); | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             error_message.set(None); | ||||
|             is_creating.set(true); | ||||
|              | ||||
|             let desc = if (*description).trim().is_empty() { | ||||
|                 None | ||||
|             } else { | ||||
|                 Some((*description).clone()) | ||||
|             }; | ||||
|              | ||||
|             on_create.emit((name.to_string(), desc, (*selected_color).clone())); | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let on_backdrop_click = { | ||||
|         let on_close = props.on_close.clone(); | ||||
|         Callback::from(move |e: MouseEvent| { | ||||
|             // Only close if clicking the backdrop, not the modal content | ||||
|             if e.target() == e.current_target() { | ||||
|                 on_close.emit(()); | ||||
|             } | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     if !props.is_open { | ||||
|         return html! {}; | ||||
|     } | ||||
|  | ||||
|     html! { | ||||
|         <div class="modal-backdrop" onclick={on_backdrop_click}> | ||||
|             <div class="create-calendar-modal"> | ||||
|                 <div class="modal-header"> | ||||
|                     <h2>{"Create New Calendar"}</h2> | ||||
|                     <button class="close-button" onclick={props.on_close.reform(|_| ())}> | ||||
|                         {"×"} | ||||
|                     </button> | ||||
|                 </div> | ||||
|                  | ||||
|                 <form class="modal-body" onsubmit={on_submit}> | ||||
|                     { | ||||
|                         if let Some(ref error) = *error_message { | ||||
|                             html! { | ||||
|                                 <div class="error-message"> | ||||
|                                     {error} | ||||
|                                 </div> | ||||
|                             } | ||||
|                         } else { | ||||
|                             html! {} | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     <div class="form-group"> | ||||
|                         <label for="calendar-name">{"Calendar Name *"}</label> | ||||
|                         <input  | ||||
|                             id="calendar-name" | ||||
|                             type="text" | ||||
|                             value={(*calendar_name).clone()} | ||||
|                             oninput={on_name_change} | ||||
|                             placeholder="Enter calendar name" | ||||
|                             maxlength="100" | ||||
|                             disabled={*is_creating} | ||||
|                         /> | ||||
|                     </div> | ||||
|                      | ||||
|                     <div class="form-group"> | ||||
|                         <label for="calendar-description">{"Description"}</label> | ||||
|                         <textarea | ||||
|                             id="calendar-description" | ||||
|                             value={(*description).clone()} | ||||
|                             oninput={on_description_change} | ||||
|                             placeholder="Optional calendar description" | ||||
|                             rows="3" | ||||
|                             disabled={*is_creating} | ||||
|                         /> | ||||
|                     </div> | ||||
|                      | ||||
|                     <div class="form-group"> | ||||
|                         <label>{"Calendar Color"}</label> | ||||
|                         <div class="color-grid"> | ||||
|                             { | ||||
|                                 props.available_colors.iter().enumerate().map(|(index, color)| { | ||||
|                                     let color = color.clone(); | ||||
|                                     let selected_color = selected_color.clone(); | ||||
|                                     let is_selected = selected_color.as_ref() == Some(&color); | ||||
|                                     let on_color_select = { | ||||
|                                         let color = color.clone(); | ||||
|                                         Callback::from(move |_: MouseEvent| { | ||||
|                                             selected_color.set(Some(color.clone())); | ||||
|                                         }) | ||||
|                                     }; | ||||
|                                      | ||||
|                                     let class_name = if is_selected {  | ||||
|                                         "color-option selected"  | ||||
|                                     } else {  | ||||
|                                         "color-option"  | ||||
|                                     }; | ||||
|                                      | ||||
|                                     html! { | ||||
|                                         <button | ||||
|                                             key={index} | ||||
|                                             type="button" | ||||
|                                             class={class_name} | ||||
|                                             style={format!("background-color: {}", color)} | ||||
|                                             onclick={on_color_select} | ||||
|                                             disabled={*is_creating} | ||||
|                                         /> | ||||
|                                     } | ||||
|                                 }).collect::<Html>() | ||||
|                             } | ||||
|                         </div> | ||||
|                         <p class="color-help-text">{"Optional: Choose a color for your calendar"}</p> | ||||
|                     </div> | ||||
|                      | ||||
|                     <div class="modal-actions"> | ||||
|                         <button  | ||||
|                             type="button"  | ||||
|                             class="cancel-button" | ||||
|                             onclick={props.on_close.reform(|_| ())} | ||||
|                             disabled={*is_creating} | ||||
|                         > | ||||
|                             {"Cancel"} | ||||
|                         </button> | ||||
|                         <button  | ||||
|                             type="submit"  | ||||
|                             class="create-button" | ||||
|                             disabled={*is_creating} | ||||
|                         > | ||||
|                             { | ||||
|                                 if *is_creating { | ||||
|                                     "Creating..." | ||||
|                                 } else { | ||||
|                                     "Create Calendar" | ||||
|                                 } | ||||
|                             } | ||||
|                         </button> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </div> | ||||
|         </div> | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone