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:
Connor Johnstone
2025-08-30 11:45:58 -04:00
parent 6887e0b389
commit 15f2d0c6d9
43 changed files with 1962 additions and 945 deletions

View 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>
}
}