Refactor create_event_modal into modular components

- Split massive 27K line modal into focused components
- Created event_form module with 6 tab components:
  * BasicDetailsTab - main event info with recurrence options properly positioned
  * AdvancedTab - status, privacy, priority
  * PeopleTab - organizer and attendees
  * CategoriesTab - event categories
  * LocationTab - location information
  * RemindersTab - reminder settings
- Added shared types and data structures
- Created new CreateEventModalV2 using modular architecture
- Recurrence options now positioned directly after repeat/reminder pickers

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-03 12:45:42 -04:00
parent 322c88612a
commit 53a62fb05e
10 changed files with 1021 additions and 0 deletions

View File

@@ -0,0 +1,208 @@
use crate::components::event_form::*;
use crate::models::ical::VEvent;
use crate::services::calendar_service::CalendarInfo;
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct CreateEventModalProps {
pub is_open: bool,
pub on_close: Callback<()>,
pub on_create: Callback<EventCreationData>,
pub available_calendars: Vec<CalendarInfo>,
pub selected_date: Option<chrono::NaiveDate>,
pub initial_start_time: Option<chrono::NaiveTime>,
pub initial_end_time: Option<chrono::NaiveTime>,
#[prop_or_default]
pub event_to_edit: Option<VEvent>,
#[prop_or_default]
pub edit_scope: Option<EditAction>,
}
#[function_component(CreateEventModalV2)]
pub fn create_event_modal_v2(props: &CreateEventModalProps) -> Html {
let active_tab = use_state(|| ModalTab::default());
let event_data = use_state(|| EventCreationData::default());
// Initialize data when modal opens or props change
use_effect_with(
(
props.is_open,
props.event_to_edit.clone(),
props.selected_date,
props.initial_start_time,
props.initial_end_time,
props.edit_scope.clone(),
props.available_calendars.clone(),
),
{
let event_data = event_data.clone();
move |_| {
if props.is_open {
let mut data = if let Some(event) = &props.event_to_edit {
// TODO: Convert VEvent to EventCreationData
EventCreationData::default()
} else if let Some(date) = props.selected_date {
let mut data = EventCreationData::default();
data.start_date = date;
data.end_date = date;
if let Some(start_time) = props.initial_start_time {
data.start_time = start_time;
}
if let Some(end_time) = props.initial_end_time {
data.end_time = end_time;
}
data
} else {
EventCreationData::default()
};
// Set default calendar
if data.selected_calendar.is_none() && !props.available_calendars.is_empty() {
data.selected_calendar = Some(props.available_calendars[0].path.clone());
}
// Set edit scope if provided
if let Some(scope) = &props.edit_scope {
data.edit_scope = Some(scope.clone());
}
event_data.set(data);
}
|| ()
}
},
);
if !props.is_open {
return html! {};
}
let on_backdrop_click = {
let on_close = props.on_close.clone();
Callback::from(move |e: MouseEvent| {
if e.target() == e.current_target() {
on_close.emit(());
}
})
};
let switch_to_tab = {
let active_tab = active_tab.clone();
Callback::from(move |tab: ModalTab| {
active_tab.set(tab);
})
};
let on_save = {
let event_data = event_data.clone();
let on_create = props.on_create.clone();
Callback::from(move |_: MouseEvent| {
on_create.emit((*event_data).clone());
})
};
let on_close = props.on_close.clone();
let tab_props = TabProps {
data: event_data.clone(),
available_calendars: props.available_calendars.clone(),
};
html! {
<div class="modal-backdrop" onclick={on_backdrop_click}>
<div class="modal-content create-event-modal">
<div class="modal-header">
<h3>
{if props.event_to_edit.is_some() { "Edit Event" } else { "Create Event" }}
</h3>
<button class="modal-close" onclick={Callback::from(move |_| on_close.emit(()))}>
{"×"}
</button>
</div>
<div class="modal-tabs">
<div class="tab-buttons">
<button
class={if *active_tab == ModalTab::BasicDetails { "tab-button active" } else { "tab-button" }}
onclick={{
let switch_to_tab = switch_to_tab.clone();
Callback::from(move |_| switch_to_tab.emit(ModalTab::BasicDetails))
}}
>
{"Basic"}
</button>
<button
class={if *active_tab == ModalTab::Advanced { "tab-button active" } else { "tab-button" }}
onclick={{
let switch_to_tab = switch_to_tab.clone();
Callback::from(move |_| switch_to_tab.emit(ModalTab::Advanced))
}}
>
{"Advanced"}
</button>
<button
class={if *active_tab == ModalTab::People { "tab-button active" } else { "tab-button" }}
onclick={{
let switch_to_tab = switch_to_tab.clone();
Callback::from(move |_| switch_to_tab.emit(ModalTab::People))
}}
>
{"People"}
</button>
<button
class={if *active_tab == ModalTab::Categories { "tab-button active" } else { "tab-button" }}
onclick={{
let switch_to_tab = switch_to_tab.clone();
Callback::from(move |_| switch_to_tab.emit(ModalTab::Categories))
}}
>
{"Categories"}
</button>
<button
class={if *active_tab == ModalTab::Location { "tab-button active" } else { "tab-button" }}
onclick={{
let switch_to_tab = switch_to_tab.clone();
Callback::from(move |_| switch_to_tab.emit(ModalTab::Location))
}}
>
{"Location"}
</button>
<button
class={if *active_tab == ModalTab::Reminders { "tab-button active" } else { "tab-button" }}
onclick={{
let switch_to_tab = switch_to_tab.clone();
Callback::from(move |_| switch_to_tab.emit(ModalTab::Reminders))
}}
>
{"Reminders"}
</button>
</div>
<div class="tab-content">
{
match *active_tab {
ModalTab::BasicDetails => html! { <BasicDetailsTab ..tab_props /> },
ModalTab::Advanced => html! { <AdvancedTab ..tab_props /> },
ModalTab::People => html! { <PeopleTab ..tab_props /> },
ModalTab::Categories => html! { <CategoriesTab ..tab_props /> },
ModalTab::Location => html! { <LocationTab ..tab_props /> },
ModalTab::Reminders => html! { <RemindersTab ..tab_props /> },
}
}
</div>
</div>
<div class="modal-footer">
<div class="modal-actions">
<button class="btn btn-secondary" onclick={Callback::from(move |_| on_close.emit(()))}>
{"Cancel"}
</button>
<button class="btn btn-primary" onclick={on_save}>
{if props.event_to_edit.is_some() { "Update Event" } else { "Create Event" }}
</button>
</div>
</div>
</div>
</div>
}
}

View File

@@ -0,0 +1,85 @@
use super::types::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlSelectElement;
use yew::prelude::*;
#[function_component(AdvancedTab)]
pub fn advanced_tab(props: &TabProps) -> Html {
let data = &props.data;
let on_status_change = {
let data = data.clone();
Callback::from(move |e: Event| {
if let Some(target) = e.target() {
if let Ok(select) = target.dyn_into::<HtmlSelectElement>() {
let mut event_data = (*data).clone();
event_data.status = match select.value().as_str() {
"tentative" => EventStatus::Tentative,
"cancelled" => EventStatus::Cancelled,
_ => EventStatus::Confirmed,
};
data.set(event_data);
}
}
})
};
let on_class_change = {
let data = data.clone();
Callback::from(move |e: Event| {
if let Some(target) = e.target() {
if let Ok(select) = target.dyn_into::<HtmlSelectElement>() {
let mut event_data = (*data).clone();
event_data.class = match select.value().as_str() {
"private" => EventClass::Private,
"confidential" => EventClass::Confidential,
_ => EventClass::Public,
};
data.set(event_data);
}
}
})
};
html! {
<div class="tab-panel">
<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"}</label>
<select id="event-priority" class="form-input">
<option value="">{"Not set"}</option>
<option value="1">{"High"}</option>
<option value="5">{"Medium"}</option>
<option value="9">{"Low"}</option>
</select>
</div>
</div>
}
}

View File

@@ -0,0 +1,338 @@
use super::types::*;
use wasm_bindgen::JsCast;
use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement};
use yew::prelude::*;
#[function_component(BasicDetailsTab)]
pub fn basic_details_tab(props: &TabProps) -> Html {
let data = &props.data;
// Event handlers
let on_title_input = {
let data = data.clone();
Callback::from(move |e: InputEvent| {
if let Some(target) = e.target() {
if let Ok(input) = target.dyn_into::<HtmlInputElement>() {
let mut event_data = (*data).clone();
event_data.title = input.value();
if !event_data.changed_fields.contains(&"title".to_string()) {
event_data.changed_fields.push("title".to_string());
}
data.set(event_data);
}
}
})
};
let on_description_input = {
let data = data.clone();
Callback::from(move |e: InputEvent| {
if let Some(target) = e.target() {
if let Ok(textarea) = target.dyn_into::<HtmlTextAreaElement>() {
let mut event_data = (*data).clone();
event_data.description = textarea.value();
data.set(event_data);
}
}
})
};
let on_calendar_change = {
let data = data.clone();
Callback::from(move |e: Event| {
if let Some(target) = e.target() {
if let Ok(select) = target.dyn_into::<HtmlSelectElement>() {
let mut event_data = (*data).clone();
let value = select.value();
let new_calendar = if value.is_empty() { None } else { Some(value) };
if event_data.selected_calendar != new_calendar {
event_data.selected_calendar = new_calendar;
if !event_data.changed_fields.contains(&"selected_calendar".to_string()) {
event_data.changed_fields.push("selected_calendar".to_string());
}
}
data.set(event_data);
}
}
})
};
let on_all_day_change = {
let data = data.clone();
Callback::from(move |e: Event| {
if let Some(target) = e.target() {
if let Ok(input) = target.dyn_into::<HtmlInputElement>() {
let mut event_data = (*data).clone();
event_data.all_day = input.checked();
data.set(event_data);
}
}
})
};
let on_recurrence_change = {
let data = data.clone();
Callback::from(move |e: Event| {
if let Some(target) = e.target() {
if let Ok(select) = target.dyn_into::<HtmlSelectElement>() {
let mut event_data = (*data).clone();
event_data.recurrence = match select.value().as_str() {
"daily" => RecurrenceType::Daily,
"weekly" => RecurrenceType::Weekly,
"monthly" => RecurrenceType::Monthly,
"yearly" => RecurrenceType::Yearly,
_ => RecurrenceType::None,
};
// Reset recurrence-related fields when changing type
event_data.recurrence_days = vec![false; 7];
event_data.recurrence_interval = 1;
event_data.recurrence_until = None;
event_data.recurrence_count = None;
event_data.monthly_by_day = None;
event_data.monthly_by_monthday = None;
event_data.yearly_by_month = vec![false; 12];
data.set(event_data);
}
}
})
};
let on_reminder_change = {
let data = data.clone();
Callback::from(move |e: Event| {
if let Some(target) = e.target() {
if let Ok(select) = target.dyn_into::<HtmlSelectElement>() {
let mut event_data = (*data).clone();
event_data.reminder = match select.value().as_str() {
"15min" => ReminderType::Minutes15,
"30min" => ReminderType::Minutes30,
"1hour" => ReminderType::Hour1,
"1day" => ReminderType::Day1,
"2days" => ReminderType::Days2,
"1week" => ReminderType::Week1,
_ => ReminderType::None,
};
data.set(event_data);
}
}
})
};
// Date/time handlers would go here...
html! {
<div class="tab-panel">
<div class="form-group">
<label for="event-title">{"Event Title *"}</label>
<input
type="text"
id="event-title"
class="form-input"
value={data.title.clone()}
oninput={on_title_input}
placeholder="Add a title"
required=true
/>
</div>
<div class="form-group">
<label for="event-description">{"Description"}</label>
<textarea
id="event-description"
class="form-input"
value={data.description.clone()}
oninput={on_description_input}
placeholder="Add a description"
rows="3"
></textarea>
</div>
<div class="form-group">
<label for="event-calendar">{"Calendar"}</label>
<select
id="event-calendar"
class="form-input"
onchange={on_calendar_change}
>
<option value="">{"Select Calendar"}</option>
{
props.available_calendars.iter().map(|calendar| {
html! {
<option
key={calendar.path.clone()}
value={calendar.path.clone()}
selected={data.selected_calendar.as_ref() == Some(&calendar.path)}
>
{&calendar.display_name}
</option>
}
}).collect::<Html>()
}
</select>
</div>
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
checked={data.all_day}
onchange={on_all_day_change}
/>
{" All Day"}
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="event-recurrence-basic">{"Repeat"}</label>
<select
id="event-recurrence-basic"
class="form-input"
onchange={on_recurrence_change}
>
<option value="none" selected={matches!(data.recurrence, RecurrenceType::None)}>{"Does not repeat"}</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 class="form-group">
<label for="event-reminder-basic">{"Reminder"}</label>
<select
id="event-reminder-basic"
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 before"}</option>
<option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes before"}</option>
<option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour before"}</option>
<option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day before"}</option>
</select>
</div>
</div>
// RECURRENCE OPTIONS GO RIGHT HERE - directly below repeat/reminder!
if matches!(data.recurrence, RecurrenceType::Weekly) {
<div class="form-group">
<label>{"Repeat on"}</label>
<div class="weekday-selection">
// Weekday checkboxes would go here
<p>{"Weekday selection will go here"}</p>
</div>
</div>
}
if !matches!(data.recurrence, RecurrenceType::None) {
<div class="recurrence-options">
<div class="form-row">
<div class="form-group">
<label>{"Every"}</label>
<div class="interval-input">
<input
type="number"
class="form-input"
value={data.recurrence_interval.to_string()}
min="1"
max="999"
/>
<span class="interval-unit">
{match data.recurrence {
RecurrenceType::Daily => if data.recurrence_interval == 1 { "day" } else { "days" },
RecurrenceType::Weekly => if data.recurrence_interval == 1 { "week" } else { "weeks" },
RecurrenceType::Monthly => if data.recurrence_interval == 1 { "month" } else { "months" },
RecurrenceType::Yearly => if data.recurrence_interval == 1 { "year" } else { "years" },
RecurrenceType::None => "",
}}
</span>
</div>
</div>
<div class="form-group">
<label>{"Ends"}</label>
<div class="end-options">
// Radio buttons for Never/Until/After would go here
<p>{"End options will go here"}</p>
</div>
</div>
</div>
// Monthly specific options
if matches!(data.recurrence, RecurrenceType::Monthly) {
<div class="form-group">
<label>{"Repeat by"}</label>
<p>{"Monthly options will go here"}</p>
</div>
}
// Yearly specific options
if matches!(data.recurrence, RecurrenceType::Yearly) {
<div class="form-group">
<label>{"Repeat in months"}</label>
<p>{"Yearly options will go here"}</p>
</div>
}
</div>
}
// Date and time fields go here AFTER recurrence options
<div class="form-row">
<div class="form-group">
<label>{"Start Date *"}</label>
<input
type="date"
class="form-input"
value={data.start_date.format("%Y-%m-%d").to_string()}
required=true
/>
</div>
if !data.all_day {
<div class="form-group">
<label>{"Start Time"}</label>
<input
type="time"
class="form-input"
value={data.start_time.format("%H:%M").to_string()}
/>
</div>
}
</div>
<div class="form-row">
<div class="form-group">
<label>{"End Date *"}</label>
<input
type="date"
class="form-input"
value={data.end_date.format("%Y-%m-%d").to_string()}
required=true
/>
</div>
if !data.all_day {
<div class="form-group">
<label>{"End Time"}</label>
<input
type="time"
class="form-input"
value={data.end_time.format("%H:%M").to_string()}
/>
</div>
}
</div>
<div class="form-group">
<label>{"Location"}</label>
<input
type="text"
class="form-input"
value={data.location.clone()}
placeholder="Enter event location"
/>
</div>
</div>
}
}

View File

@@ -0,0 +1,39 @@
use super::types::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::prelude::*;
#[function_component(CategoriesTab)]
pub fn categories_tab(props: &TabProps) -> Html {
let data = &props.data;
let on_categories_input = {
let data = data.clone();
Callback::from(move |e: InputEvent| {
if let Some(target) = e.target() {
if let Ok(input) = target.dyn_into::<HtmlInputElement>() {
let mut event_data = (*data).clone();
event_data.categories = input.value();
data.set(event_data);
}
}
})
};
html! {
<div class="tab-panel">
<div class="form-group">
<label for="event-categories">{"Categories"}</label>
<input
type="text"
id="event-categories"
class="form-input"
value={data.categories.clone()}
oninput={on_categories_input}
placeholder="Add categories (comma-separated)"
/>
<p class="form-help-text">{"Add categories to help organize your events. Separate multiple categories with commas."}</p>
</div>
</div>
}
}

View File

@@ -0,0 +1,39 @@
use super::types::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
use yew::prelude::*;
#[function_component(LocationTab)]
pub fn location_tab(props: &TabProps) -> Html {
let data = &props.data;
let on_location_input = {
let data = data.clone();
Callback::from(move |e: InputEvent| {
if let Some(target) = e.target() {
if let Ok(input) = target.dyn_into::<HtmlInputElement>() {
let mut event_data = (*data).clone();
event_data.location = input.value();
data.set(event_data);
}
}
})
};
html! {
<div class="tab-panel">
<div class="form-group">
<label for="event-location">{"Location"}</label>
<input
type="text"
id="event-location"
class="form-input"
value={data.location.clone()}
oninput={on_location_input}
placeholder="Enter event location"
/>
<p class="form-help-text">{"Add the location where the event will take place."}</p>
</div>
</div>
}
}

View File

@@ -0,0 +1,16 @@
// Event form components module
pub mod types;
pub mod basic_details;
pub mod advanced;
pub mod people;
pub mod categories;
pub mod location;
pub mod reminders;
pub use types::*;
pub use basic_details::BasicDetailsTab;
pub use advanced::AdvancedTab;
pub use people::PeopleTab;
pub use categories::CategoriesTab;
pub use location::LocationTab;
pub use reminders::RemindersTab;

View File

@@ -0,0 +1,63 @@
use super::types::*;
use wasm_bindgen::JsCast;
use web_sys::{HtmlInputElement, HtmlTextAreaElement};
use yew::prelude::*;
#[function_component(PeopleTab)]
pub fn people_tab(props: &TabProps) -> Html {
let data = &props.data;
let on_organizer_input = {
let data = data.clone();
Callback::from(move |e: InputEvent| {
if let Some(target) = e.target() {
if let Ok(input) = target.dyn_into::<HtmlInputElement>() {
let mut event_data = (*data).clone();
event_data.organizer = input.value();
data.set(event_data);
}
}
})
};
let on_attendees_input = {
let data = data.clone();
Callback::from(move |e: InputEvent| {
if let Some(target) = e.target() {
if let Ok(textarea) = target.dyn_into::<HtmlTextAreaElement>() {
let mut event_data = (*data).clone();
event_data.attendees = textarea.value();
data.set(event_data);
}
}
})
};
html! {
<div class="tab-panel">
<div class="form-group">
<label for="event-organizer">{"Organizer"}</label>
<input
type="text"
id="event-organizer"
class="form-input"
value={data.organizer.clone()}
oninput={on_organizer_input}
placeholder="Event organizer"
/>
</div>
<div class="form-group">
<label for="event-attendees">{"Attendees"}</label>
<textarea
id="event-attendees"
class="form-input"
value={data.attendees.clone()}
oninput={on_attendees_input}
placeholder="Add attendees (one per line)"
rows="4"
></textarea>
</div>
</div>
}
}

View File

@@ -0,0 +1,52 @@
use super::types::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlSelectElement;
use yew::prelude::*;
#[function_component(RemindersTab)]
pub fn reminders_tab(props: &TabProps) -> Html {
let data = &props.data;
let on_reminder_change = {
let data = data.clone();
Callback::from(move |e: Event| {
if let Some(target) = e.target() {
if let Ok(select) = target.dyn_into::<HtmlSelectElement>() {
let mut event_data = (*data).clone();
event_data.reminder = match select.value().as_str() {
"15min" => ReminderType::Minutes15,
"30min" => ReminderType::Minutes30,
"1hour" => ReminderType::Hour1,
"1day" => ReminderType::Day1,
"2days" => ReminderType::Days2,
"1week" => ReminderType::Week1,
_ => ReminderType::None,
};
data.set(event_data);
}
}
})
};
html! {
<div class="tab-panel">
<div class="form-group">
<label for="event-reminder">{"Default 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 before"}</option>
<option value="30min" selected={matches!(data.reminder, ReminderType::Minutes30)}>{"30 minutes before"}</option>
<option value="1hour" selected={matches!(data.reminder, ReminderType::Hour1)}>{"1 hour before"}</option>
<option value="1day" selected={matches!(data.reminder, ReminderType::Day1)}>{"1 day before"}</option>
<option value="2days" selected={matches!(data.reminder, ReminderType::Days2)}>{"2 days before"}</option>
<option value="1week" selected={matches!(data.reminder, ReminderType::Week1)}>{"1 week before"}</option>
</select>
<p class="form-help-text">{"Set when you want to be reminded about this event."}</p>
</div>
</div>
}
}

View File

@@ -0,0 +1,180 @@
use crate::models::ical::VEvent;
use crate::services::calendar_service::CalendarInfo;
use chrono::{Datelike, Local, NaiveDate, NaiveTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use yew::prelude::*;
#[derive(Clone, PartialEq, Debug)]
pub enum EventStatus {
Confirmed,
Tentative,
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,
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)]
pub enum ModalTab {
BasicDetails,
Advanced,
People,
Categories,
Location,
Reminders,
}
impl Default for ModalTab {
fn default() -> Self {
ModalTab::BasicDetails
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum EditAction {
ThisOnly,
ThisAndFuture,
AllInSeries,
}
#[derive(Clone, PartialEq, Debug)]
pub struct EventCreationData {
// Basic event info
pub title: String,
pub description: String,
pub location: String,
pub all_day: bool,
// Timing
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub start_time: NaiveTime,
pub end_time: NaiveTime,
// Classification
pub status: EventStatus,
pub class: EventClass,
pub priority: Option<u8>,
// People
pub organizer: String,
pub attendees: String,
// Categorization
pub categories: String,
// Reminders
pub reminder: ReminderType,
// Recurrence
pub recurrence: RecurrenceType,
pub recurrence_interval: u32,
pub recurrence_until: Option<NaiveDate>,
pub recurrence_count: Option<u32>,
pub recurrence_days: Vec<bool>, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat]
// Advanced recurrence
pub monthly_by_day: Option<String>, // e.g., "1MO" for first Monday
pub monthly_by_monthday: Option<u8>, // e.g., 15 for 15th day of month
pub yearly_by_month: Vec<bool>, // [Jan, Feb, Mar, ...]
// Calendar selection
pub selected_calendar: Option<String>,
// Edit tracking (for recurring events)
pub edit_scope: Option<EditAction>,
pub changed_fields: Vec<String>,
}
impl Default for EventCreationData {
fn default() -> Self {
let now_local = Local::now();
let start_date = now_local.date_naive();
let start_time = NaiveTime::from_hms_opt(9, 0, 0).unwrap_or_default();
let end_time = NaiveTime::from_hms_opt(10, 0, 0).unwrap_or_default();
Self {
title: String::new(),
description: String::new(),
location: String::new(),
all_day: false,
start_date,
end_date: start_date,
start_time,
end_time,
status: EventStatus::default(),
class: EventClass::default(),
priority: None,
organizer: String::new(),
attendees: String::new(),
categories: String::new(),
reminder: ReminderType::default(),
recurrence: RecurrenceType::default(),
recurrence_interval: 1,
recurrence_until: None,
recurrence_count: None,
recurrence_days: vec![false; 7],
monthly_by_day: None,
monthly_by_monthday: None,
yearly_by_month: vec![false; 12],
selected_calendar: None,
edit_scope: None,
changed_fields: vec![],
}
}
}
// Common props for all tab components
#[derive(Properties, PartialEq)]
pub struct TabProps {
pub data: UseStateHandle<EventCreationData>,
pub available_calendars: Vec<CalendarInfo>,
}

View File

@@ -6,6 +6,7 @@ pub mod context_menu;
pub mod create_calendar_modal; pub mod create_calendar_modal;
pub mod create_event_modal; pub mod create_event_modal;
pub mod event_context_menu; pub mod event_context_menu;
pub mod event_form;
pub mod event_modal; pub mod event_modal;
pub mod login; pub mod login;
pub mod month_view; pub mod month_view;