diff --git a/frontend/src/components/create_event_modal_v2.rs b/frontend/src/components/create_event_modal_v2.rs new file mode 100644 index 0000000..326c97a --- /dev/null +++ b/frontend/src/components/create_event_modal_v2.rs @@ -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, + pub available_calendars: Vec, + pub selected_date: Option, + pub initial_start_time: Option, + pub initial_end_time: Option, + #[prop_or_default] + pub event_to_edit: Option, + #[prop_or_default] + pub edit_scope: Option, +} + +#[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! { + + } +} \ No newline at end of file diff --git a/frontend/src/components/event_form/advanced.rs b/frontend/src/components/event_form/advanced.rs new file mode 100644 index 0000000..8f6b02e --- /dev/null +++ b/frontend/src/components/event_form/advanced.rs @@ -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::() { + 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::() { + 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! { +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/components/event_form/basic_details.rs b/frontend/src/components/event_form/basic_details.rs new file mode 100644 index 0000000..310f9e9 --- /dev/null +++ b/frontend/src/components/event_form/basic_details.rs @@ -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::() { + 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::() { + 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::() { + 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::() { + 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::() { + 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::() { + 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! { +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ + // RECURRENCE OPTIONS GO RIGHT HERE - directly below repeat/reminder! + if matches!(data.recurrence, RecurrenceType::Weekly) { +
+ +
+ // Weekday checkboxes would go here +

{"Weekday selection will go here"}

+
+
+ } + + if !matches!(data.recurrence, RecurrenceType::None) { +
+
+
+ +
+ + + {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 => "", + }} + +
+
+ +
+ +
+ // Radio buttons for Never/Until/After would go here +

{"End options will go here"}

+
+
+
+ + // Monthly specific options + if matches!(data.recurrence, RecurrenceType::Monthly) { +
+ +

{"Monthly options will go here"}

+
+ } + + // Yearly specific options + if matches!(data.recurrence, RecurrenceType::Yearly) { +
+ +

{"Yearly options will go here"}

+
+ } +
+ } + + // Date and time fields go here AFTER recurrence options +
+
+ + +
+ + if !data.all_day { +
+ + +
+ } +
+ +
+
+ + +
+ + if !data.all_day { +
+ + +
+ } +
+ +
+ + +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/components/event_form/categories.rs b/frontend/src/components/event_form/categories.rs new file mode 100644 index 0000000..f02896a --- /dev/null +++ b/frontend/src/components/event_form/categories.rs @@ -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::() { + let mut event_data = (*data).clone(); + event_data.categories = input.value(); + data.set(event_data); + } + } + }) + }; + + html! { +
+
+ + +

{"Add categories to help organize your events. Separate multiple categories with commas."}

+
+
+ } +} \ No newline at end of file diff --git a/frontend/src/components/event_form/location.rs b/frontend/src/components/event_form/location.rs new file mode 100644 index 0000000..c3bfae1 --- /dev/null +++ b/frontend/src/components/event_form/location.rs @@ -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::() { + let mut event_data = (*data).clone(); + event_data.location = input.value(); + data.set(event_data); + } + } + }) + }; + + html! { +
+
+ + +

{"Add the location where the event will take place."}

+
+
+ } +} \ No newline at end of file diff --git a/frontend/src/components/event_form/mod.rs b/frontend/src/components/event_form/mod.rs new file mode 100644 index 0000000..ed273f8 --- /dev/null +++ b/frontend/src/components/event_form/mod.rs @@ -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; \ No newline at end of file diff --git a/frontend/src/components/event_form/people.rs b/frontend/src/components/event_form/people.rs new file mode 100644 index 0000000..a56c9db --- /dev/null +++ b/frontend/src/components/event_form/people.rs @@ -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::() { + 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::() { + let mut event_data = (*data).clone(); + event_data.attendees = textarea.value(); + data.set(event_data); + } + } + }) + }; + + html! { +
+
+ + +
+ +
+ + +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/components/event_form/reminders.rs b/frontend/src/components/event_form/reminders.rs new file mode 100644 index 0000000..4601108 --- /dev/null +++ b/frontend/src/components/event_form/reminders.rs @@ -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::() { + 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! { +
+
+ + +

{"Set when you want to be reminded about this event."}

+
+
+ } +} \ No newline at end of file diff --git a/frontend/src/components/event_form/types.rs b/frontend/src/components/event_form/types.rs new file mode 100644 index 0000000..d37b2cc --- /dev/null +++ b/frontend/src/components/event_form/types.rs @@ -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, + + // 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, + pub recurrence_count: Option, + pub recurrence_days: Vec, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] + + // Advanced recurrence + pub monthly_by_day: Option, // e.g., "1MO" for first Monday + pub monthly_by_monthday: Option, // e.g., 15 for 15th day of month + pub yearly_by_month: Vec, // [Jan, Feb, Mar, ...] + + // Calendar selection + pub selected_calendar: Option, + + // Edit tracking (for recurring events) + pub edit_scope: Option, + pub changed_fields: Vec, +} + +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, + pub available_calendars: Vec, +} \ No newline at end of file diff --git a/frontend/src/components/mod.rs b/frontend/src/components/mod.rs index 3a3a6fb..ecc035e 100644 --- a/frontend/src/components/mod.rs +++ b/frontend/src/components/mod.rs @@ -6,6 +6,7 @@ pub mod context_menu; pub mod create_calendar_modal; pub mod create_event_modal; pub mod event_context_menu; +pub mod event_form; pub mod event_modal; pub mod login; pub mod month_view;