diff --git a/backend/src/handlers/events.rs b/backend/src/handlers/events.rs index af413eb..1ebe2e1 100644 --- a/backend/src/handlers/events.rs +++ b/backend/src/handlers/events.rs @@ -522,19 +522,8 @@ pub async fn create_event( .collect() }; - // Parse alarms - convert from minutes string to EventReminder structs - let alarms: Vec = if request.reminder.trim().is_empty() { - Vec::new() - } else { - match request.reminder.parse::() { - Ok(minutes) => vec![crate::calendar::EventReminder { - minutes_before: minutes, - action: crate::calendar::ReminderAction::Display, - description: None, - }], - Err(_) => Vec::new(), - } - }; + // Use VAlarms directly from request (no conversion needed) + let alarms = request.alarms; // Check if recurrence is already a full RRULE or just a simple type let rrule = if request.recurrence.starts_with("FREQ=") { @@ -645,21 +634,7 @@ pub async fn create_event( event.categories = categories; event.rrule = rrule; event.all_day = request.all_day; - event.alarms = alarms - .into_iter() - .map(|reminder| VAlarm { - action: AlarmAction::Display, - trigger: AlarmTrigger::Duration(chrono::Duration::minutes( - -reminder.minutes_before as i64, - )), - duration: None, - repeat: None, - description: reminder.description, - summary: None, - attendees: Vec::new(), - attach: Vec::new(), - }) - .collect(); + event.alarms = alarms; event.calendar_path = Some(calendar_path.clone()); // Create the event on the CalDAV server diff --git a/backend/src/models.rs b/backend/src/models.rs index c637712..e515058 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -3,6 +3,7 @@ use axum::{ response::{IntoResponse, Response}, Json, }; +use calendar_models::VAlarm; use serde::{Deserialize, Serialize}; // API request/response types @@ -113,7 +114,7 @@ pub struct CreateEventRequest { pub organizer: String, // organizer email pub attendees: String, // comma-separated attendee emails pub categories: String, // comma-separated categories - pub reminder: String, // reminder type + pub alarms: Vec, // event alarms pub recurrence: String, // recurrence type pub recurrence_days: Vec, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence pub calendar_path: Option, // Optional - use first calendar if not specified @@ -144,7 +145,7 @@ pub struct UpdateEventRequest { pub organizer: String, // organizer email pub attendees: String, // comma-separated attendee emails pub categories: String, // comma-separated categories - pub reminder: String, // reminder type + pub alarms: Vec, // event alarms pub recurrence: String, // recurrence type pub recurrence_days: Vec, // [Sun, Mon, Tue, Wed, Thu, Fri, Sat] for weekly recurrence pub recurrence_interval: Option, // Every N days/weeks/months/years @@ -181,7 +182,7 @@ pub struct CreateEventSeriesRequest { pub organizer: String, // organizer email pub attendees: String, // comma-separated attendee emails pub categories: String, // comma-separated categories - pub reminder: String, // reminder type + pub alarms: Vec, // event alarms // Series-specific fields pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly) @@ -219,7 +220,7 @@ pub struct UpdateEventSeriesRequest { pub organizer: String, // organizer email pub attendees: String, // comma-separated attendee emails pub categories: String, // comma-separated categories - pub reminder: String, // reminder type + pub alarms: Vec, // event alarms // Series-specific fields pub recurrence: String, // recurrence type (daily, weekly, monthly, yearly) diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 78a4847..0520ee6 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -1147,7 +1147,7 @@ pub fn App() -> Html { .collect::>() .join(","), original_event.categories.join(","), - reminder_str.clone(), + original_event.alarms.clone(), recurrence_str.clone(), vec![false; 7], // recurrence_days 1, // recurrence_interval - default for drag-and-drop @@ -1204,7 +1204,7 @@ pub fn App() -> Html { .collect::>() .join(","), original_event.categories.join(","), - reminder_str, + original_event.alarms.clone(), recurrence_str, recurrence_days, 1, // recurrence_interval - default to 1 for drag-and-drop diff --git a/frontend/src/components/create_event_modal.rs b/frontend/src/components/create_event_modal.rs index 8b4b44a..d493d12 100644 --- a/frontend/src/components/create_event_modal.rs +++ b/frontend/src/components/create_event_modal.rs @@ -292,8 +292,8 @@ fn vevent_to_creation_data(event: &crate::models::ical::VEvent, available_calend // Categorization categories: event.categories.join(","), - // Reminders - TODO: Parse alarm from VEvent if needed - reminder: ReminderType::None, + // Reminders - Use VAlarms from the event + alarms: event.alarms.clone(), // Recurrence - Parse RRULE if present recurrence: if let Some(ref rrule_str) = event.rrule { diff --git a/frontend/src/components/event_form/add_alarm_modal.rs b/frontend/src/components/event_form/add_alarm_modal.rs new file mode 100644 index 0000000..bbb3b18 --- /dev/null +++ b/frontend/src/components/event_form/add_alarm_modal.rs @@ -0,0 +1,296 @@ +use calendar_models::{VAlarm, AlarmAction, AlarmTrigger}; +use chrono::{Duration, DateTime, Utc, NaiveTime}; +use wasm_bindgen::JsCast; +use web_sys::{HtmlSelectElement, HtmlInputElement}; +use yew::prelude::*; + +#[derive(Clone, PartialEq)] +pub enum TriggerType { + Relative, // Duration before/after event + Absolute, // Specific date/time +} + +#[derive(Clone, PartialEq)] +pub enum RelativeTo { + Start, + End, +} + +#[derive(Clone, PartialEq)] +pub enum TimeUnit { + Minutes, + Hours, + Days, + Weeks, +} + +#[derive(Properties, PartialEq)] +pub struct AddAlarmModalProps { + pub is_open: bool, + pub editing_index: Option, // If editing an existing alarm + pub initial_alarm: Option, // For editing mode + pub on_close: Callback<()>, + pub on_save: Callback, +} + +#[function_component(AddAlarmModal)] +pub fn add_alarm_modal(props: &AddAlarmModalProps) -> Html { + // Form state + let trigger_type = use_state(|| TriggerType::Relative); + let relative_to = use_state(|| RelativeTo::Start); + let time_unit = use_state(|| TimeUnit::Minutes); + let time_value = use_state(|| 15i32); + let before_after = use_state(|| true); // true = before, false = after + let absolute_date = use_state(|| chrono::Local::now().date_naive()); + let absolute_time = use_state(|| NaiveTime::from_hms_opt(9, 0, 0).unwrap()); + + // Initialize form with existing alarm data if editing + { + let trigger_type = trigger_type.clone(); + let time_value = time_value.clone(); + + use_effect_with(props.initial_alarm.clone(), move |initial_alarm| { + if let Some(alarm) = initial_alarm { + match &alarm.trigger { + AlarmTrigger::Duration(duration) => { + trigger_type.set(TriggerType::Relative); + let minutes = duration.num_minutes().abs(); + time_value.set(minutes as i32); + } + AlarmTrigger::DateTime(_) => { + trigger_type.set(TriggerType::Absolute); + } + } + } + }); + } + + let on_trigger_type_change = { + let trigger_type = trigger_type.clone(); + Callback::from(move |e: Event| { + if let Some(target) = e.target_dyn_into::() { + let new_type = match target.value().as_str() { + "absolute" => TriggerType::Absolute, + _ => TriggerType::Relative, + }; + trigger_type.set(new_type); + } + }) + }; + + + let on_relative_to_change = { + let relative_to = relative_to.clone(); + Callback::from(move |e: Event| { + if let Some(target) = e.target_dyn_into::() { + let new_relative = match target.value().as_str() { + "end" => RelativeTo::End, + _ => RelativeTo::Start, + }; + relative_to.set(new_relative); + } + }) + }; + + let on_time_unit_change = { + let time_unit = time_unit.clone(); + Callback::from(move |e: Event| { + if let Some(target) = e.target_dyn_into::() { + let new_unit = match target.value().as_str() { + "hours" => TimeUnit::Hours, + "days" => TimeUnit::Days, + "weeks" => TimeUnit::Weeks, + _ => TimeUnit::Minutes, + }; + time_unit.set(new_unit); + } + }) + }; + + let on_time_value_change = { + let time_value = time_value.clone(); + Callback::from(move |e: Event| { + if let Some(target) = e.target_dyn_into::() { + if let Ok(value) = target.value().parse::() { + time_value.set(value.max(1)); // Minimum 1 + } + } + }) + }; + + let on_before_after_change = { + let before_after = before_after.clone(); + Callback::from(move |e: Event| { + if let Some(target) = e.target_dyn_into::() { + let is_before = target.value() == "before"; + before_after.set(is_before); + } + }) + }; + + + + let on_save_click = { + let trigger_type = trigger_type.clone(); + let time_unit = time_unit.clone(); + let time_value = time_value.clone(); + let before_after = before_after.clone(); + let absolute_date = absolute_date.clone(); + let absolute_time = absolute_time.clone(); + let on_save = props.on_save.clone(); + + Callback::from(move |_| { + // Create the alarm trigger + let trigger = match *trigger_type { + TriggerType::Relative => { + let minutes = match *time_unit { + TimeUnit::Minutes => *time_value, + TimeUnit::Hours => *time_value * 60, + TimeUnit::Days => *time_value * 60 * 24, + TimeUnit::Weeks => *time_value * 60 * 24 * 7, + }; + + let signed_minutes = if *before_after { -minutes } else { minutes } as i64; + AlarmTrigger::Duration(Duration::minutes(signed_minutes)) + } + TriggerType::Absolute => { + // Combine date and time to create a DateTime + let naive_datetime = absolute_date.and_time(*absolute_time); + let utc_datetime = DateTime::from_naive_utc_and_offset(naive_datetime, Utc); + AlarmTrigger::DateTime(utc_datetime) + } + }; + + // Create the VAlarm - always use Display action, no custom description + let alarm = VAlarm { + action: AlarmAction::Display, + trigger, + duration: None, + repeat: None, + description: None, + summary: None, + attendees: Vec::new(), + attach: Vec::new(), + }; + + on_save.emit(alarm); + }) + }; + + let on_backdrop_click = { + let on_close = props.on_close.clone(); + Callback::from(move |e: MouseEvent| { + if let Some(target) = e.target() { + if let Some(element) = target.dyn_ref::() { + if element.class_list().contains("add-alarm-backdrop") { + on_close.emit(()); + } + } + } + }) + }; + + if !props.is_open { + return html! {}; + } + + html! { +
+
+
+

{ + if props.editing_index.is_some() { + "Edit Reminder" + } else { + "Add Reminder" + } + }

+ +
+ +
+ // Trigger Type Selection +
+ + +
+ + // Relative Trigger Configuration + if matches!(*trigger_type, TriggerType::Relative) { +
+ +
+ + + + +
+
+ } + + // Absolute Trigger Configuration + if matches!(*trigger_type, TriggerType::Absolute) { +
+ +
+ + +
+
+ } + + +
+ + +
+
+ } +} \ No newline at end of file diff --git a/frontend/src/components/event_form/alarm_list.rs b/frontend/src/components/event_form/alarm_list.rs new file mode 100644 index 0000000..26be870 --- /dev/null +++ b/frontend/src/components/event_form/alarm_list.rs @@ -0,0 +1,133 @@ +use calendar_models::{VAlarm, AlarmAction, AlarmTrigger}; +use chrono::Duration; +use yew::prelude::*; + +#[derive(Properties, PartialEq)] +pub struct AlarmListProps { + pub alarms: Vec, + pub on_alarm_delete: Callback, // Index of alarm to delete + pub on_alarm_edit: Callback, // Index of alarm to edit +} + +#[function_component(AlarmList)] +pub fn alarm_list(props: &AlarmListProps) -> Html { + if props.alarms.is_empty() { + return html! { +
+

{"No reminders set"}

+

{"Click 'Add Reminder' to create your first reminder"}

+
+ }; + } + + html! { +
+
{"Configured Reminders"}
+
+ { + props.alarms.iter().enumerate().map(|(index, alarm)| { + let alarm_description = format_alarm_description(alarm); + let action_icon = get_action_icon(&alarm.action); + + let on_delete = { + let on_alarm_delete = props.on_alarm_delete.clone(); + Callback::from(move |_| { + on_alarm_delete.emit(index); + }) + }; + + let on_edit = { + let on_alarm_edit = props.on_alarm_edit.clone(); + Callback::from(move |_| { + on_alarm_edit.emit(index); + }) + }; + + html! { +
+
+ {action_icon} + {alarm_description} +
+
+ + +
+
+ } + }).collect::() + } +
+
+ } +} + +/// Format alarm description for display +fn format_alarm_description(alarm: &VAlarm) -> String { + match &alarm.trigger { + AlarmTrigger::Duration(duration) => { + format_duration_description(duration) + } + AlarmTrigger::DateTime(datetime) => { + format!("At {}", datetime.format("%Y-%m-%d %H:%M UTC")) + } + } +} + +/// Get icon for alarm action - always use bell for consistent notification type +fn get_action_icon(_action: &AlarmAction) -> Html { + html! { } +} + +/// Format duration for human-readable description +fn format_duration_description(duration: &Duration) -> String { + let minutes = duration.num_minutes(); + + if minutes == 0 { + return "At event time".to_string(); + } + + let abs_minutes = minutes.abs(); + let before_or_after = if minutes < 0 { "before" } else { "after" }; + + // Convert to human-readable format + if abs_minutes >= 60 * 24 * 7 { + let weeks = abs_minutes / (60 * 24 * 7); + let remainder = abs_minutes % (60 * 24 * 7); + if remainder == 0 { + format!("{} week{} {}", weeks, if weeks == 1 { "" } else { "s" }, before_or_after) + } else { + format!("{} minutes {}", abs_minutes, before_or_after) + } + } else if abs_minutes >= 60 * 24 { + let days = abs_minutes / (60 * 24); + let remainder = abs_minutes % (60 * 24); + if remainder == 0 { + format!("{} day{} {}", days, if days == 1 { "" } else { "s" }, before_or_after) + } else { + format!("{} minutes {}", abs_minutes, before_or_after) + } + } else if abs_minutes >= 60 { + let hours = abs_minutes / 60; + let remainder = abs_minutes % 60; + if remainder == 0 { + format!("{} hour{} {}", hours, if hours == 1 { "" } else { "s" }, before_or_after) + } else { + format!("{} minutes {}", abs_minutes, before_or_after) + } + } else { + format!("{} minute{} {}", abs_minutes, if abs_minutes == 1 { "" } else { "s" }, before_or_after) + } +} \ 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 index a5bec2b..f867cc6 100644 --- a/frontend/src/components/event_form/basic_details.rs +++ b/frontend/src/components/event_form/basic_details.rs @@ -99,26 +99,13 @@ pub fn basic_details_tab(props: &TabProps) -> Html { }) }; - 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); - } - } - }) - }; + // TODO: Replace with new alarm management UI + // let on_reminder_change = { + // let data = data.clone(); + // Callback::from(move |e: Event| { + // // Will be replaced with VAlarm management + // }) + // }; let on_recurrence_interval_change = { let data = data.clone(); @@ -321,42 +308,31 @@ pub fn basic_details_tab(props: &TabProps) -> Html { > -
- - -
- -
- -
-
+
+ + +
+
- -
- - -
// RECURRENCE OPTIONS GO RIGHT HERE - directly below repeat/reminder! @@ -659,6 +620,18 @@ pub fn basic_details_tab(props: &TabProps) -> Html { } + // All Day checkbox above date/time fields +
+ +
+ // Date and time fields go here AFTER recurrence options
diff --git a/frontend/src/components/event_form/mod.rs b/frontend/src/components/event_form/mod.rs index ed273f8..95e5498 100644 --- a/frontend/src/components/event_form/mod.rs +++ b/frontend/src/components/event_form/mod.rs @@ -1,5 +1,7 @@ // Event form components module pub mod types; +pub mod alarm_list; +pub mod add_alarm_modal; pub mod basic_details; pub mod advanced; pub mod people; @@ -8,6 +10,8 @@ pub mod location; pub mod reminders; pub use types::*; +pub use alarm_list::AlarmList; +pub use add_alarm_modal::AddAlarmModal; pub use basic_details::BasicDetailsTab; pub use advanced::AdvancedTab; pub use people::PeopleTab; diff --git a/frontend/src/components/event_form/reminders.rs b/frontend/src/components/event_form/reminders.rs index 82cb095..41b3f7d 100644 --- a/frontend/src/components/event_form/reminders.rs +++ b/frontend/src/components/event_form/reminders.rs @@ -1,100 +1,116 @@ -use super::types::*; -// Types are already imported from super::types::* -use wasm_bindgen::JsCast; -use web_sys::HtmlSelectElement; +use super::{types::*, AlarmList, AddAlarmModal}; +use calendar_models::VAlarm; use yew::prelude::*; #[function_component(RemindersTab)] pub fn reminders_tab(props: &TabProps) -> Html { let data = &props.data; - let on_reminder_change = { + // Modal state + let is_modal_open = use_state(|| false); + let editing_index = use_state(|| None::); + + // Add alarm callback + let on_add_alarm = { + let is_modal_open = is_modal_open.clone(); + let editing_index = editing_index.clone(); + Callback::from(move |_| { + editing_index.set(None); + is_modal_open.set(true); + }) + }; + + // Edit alarm callback + let on_alarm_edit = { + let is_modal_open = is_modal_open.clone(); + let editing_index = editing_index.clone(); + Callback::from(move |index: usize| { + editing_index.set(Some(index)); + is_modal_open.set(true); + }) + }; + + // Delete alarm callback + let on_alarm_delete = { 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); - } + Callback::from(move |index: usize| { + let mut current_data = (*data).clone(); + if index < current_data.alarms.len() { + current_data.alarms.remove(index); + data.set(current_data); } }) }; + + // Close modal callback + let on_modal_close = { + let is_modal_open = is_modal_open.clone(); + let editing_index = editing_index.clone(); + Callback::from(move |_| { + is_modal_open.set(false); + editing_index.set(None); + }) + }; + + // Save alarm callback + let on_alarm_save = { + let data = data.clone(); + let is_modal_open = is_modal_open.clone(); + let editing_index = editing_index.clone(); + Callback::from(move |alarm: VAlarm| { + let mut current_data = (*data).clone(); + + if let Some(index) = *editing_index { + // Edit existing alarm + if index < current_data.alarms.len() { + current_data.alarms[index] = alarm; + } + } else { + // Add new alarm + current_data.alarms.push(alarm); + } + + data.set(current_data); + is_modal_open.set(false); + editing_index.set(None); + }) + }; + + // Get initial alarm for editing + let initial_alarm = (*editing_index).and_then(|index| { + data.alarms.get(index).cloned() + }); html! {
- - -

{"Choose when you'd like to be reminded about this event"}

-
- -
-
{"Reminder & Alarm Types"}
-
-
- {"Display Alarm"} -

{"Pop-up notification on your device"}

-
-
- {"Email Reminder"} -

{"Email notification sent to your address"}

-
-
- {"Audio Alert"} -

{"Sound notification with custom audio"}

-
-
- {"SMS/Text"} -

{"Text message reminder (enterprise feature)"}

-
+
+
{"Event Reminders"}
+
-

{"Multiple alarm types follow RFC 5545 VALARM standards"}

+

{"Configure multiple reminders with custom timing and notification types"}

-
-
{"Advanced Reminder Features"}
-
    -
  • {"Multiple reminders per event with different timing"}
  • -
  • {"Custom reminder messages and descriptions"}
  • -
  • {"Recurring reminders for recurring events"}
  • -
  • {"Snooze and dismiss functionality"}
  • -
  • {"Integration with system notifications"}
  • -
+ -
-
{"File Attachments & Documents"}
-

{"Future attachment features will include:"}

-
    -
  • {"Drag-and-drop file uploads"}
  • -
  • {"Document preview and thumbnails"}
  • -
  • {"Cloud storage integration (Google Drive, OneDrive)"}
  • -
  • {"Version control for updated documents"}
  • -
  • {"Shared access permissions for attendees"}
  • -
-

{"Attachment functionality will be implemented in a future release."}

-
-
+
} } \ No newline at end of file diff --git a/frontend/src/components/event_form/types.rs b/frontend/src/components/event_form/types.rs index 68efc3b..1040087 100644 --- a/frontend/src/components/event_form/types.rs +++ b/frontend/src/components/event_form/types.rs @@ -1,6 +1,7 @@ use crate::services::calendar_service::CalendarInfo; use chrono::{Local, NaiveDate, NaiveTime}; use yew::prelude::*; +use calendar_models::VAlarm; #[derive(Clone, PartialEq, Debug)] pub enum EventStatus { @@ -104,8 +105,8 @@ pub struct EventCreationData { // Categorization pub categories: String, - // Reminders - pub reminder: ReminderType, + // Reminders/Alarms + pub alarms: Vec, // Recurrence pub recurrence: RecurrenceType, @@ -145,7 +146,7 @@ impl EventCreationData { String, // organizer String, // attendees String, // categories - String, // reminder + Vec, // alarms String, // recurrence Vec, // recurrence_days u32, // recurrence_interval @@ -196,7 +197,7 @@ impl EventCreationData { self.organizer.clone(), self.attendees.clone(), self.categories.clone(), - format!("{:?}", self.reminder), + self.alarms.clone(), format!("{:?}", self.recurrence), self.recurrence_days.clone(), self.recurrence_interval, @@ -230,7 +231,7 @@ impl Default for EventCreationData { organizer: String::new(), attendees: String::new(), categories: String::new(), - reminder: ReminderType::default(), + alarms: Vec::new(), recurrence: RecurrenceType::default(), recurrence_interval: 1, recurrence_until: None, diff --git a/frontend/src/components/month_view.rs b/frontend/src/components/month_view.rs index 4cb8350..7736679 100644 --- a/frontend/src/components/month_view.rs +++ b/frontend/src/components/month_view.rs @@ -213,7 +213,10 @@ pub fn month_view(props: &MonthViewProps) -> Html { {onclick} {oncontextmenu} > - {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} + {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} + if !event.alarms.is_empty() { + + }
} }).collect::() diff --git a/frontend/src/components/week_view.rs b/frontend/src/components/week_view.rs index b265f27..8bd7ae3 100644 --- a/frontend/src/components/week_view.rs +++ b/frontend/src/components/week_view.rs @@ -968,7 +968,12 @@ pub fn week_view(props: &WeekViewProps) -> Html { // Event content
-
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
+
+ {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} + if !event.alarms.is_empty() { + + } +
{if !is_all_day && duration_pixels > 30.0 { html! {
{time_display}
} } else { diff --git a/frontend/src/services/calendar_service.rs b/frontend/src/services/calendar_service.rs index fe44654..3309044 100644 --- a/frontend/src/services/calendar_service.rs +++ b/frontend/src/services/calendar_service.rs @@ -6,8 +6,8 @@ use wasm_bindgen::JsCast; use wasm_bindgen_futures::JsFuture; use web_sys::{Request, RequestInit, RequestMode, Response}; -// Import RFC 5545 compliant VEvent from shared library -use calendar_models::VEvent; +// Import RFC 5545 compliant VEvent and VAlarm from shared library +use calendar_models::{VEvent, VAlarm}; // Create type alias for backward compatibility pub type CalendarEvent = VEvent; @@ -1250,7 +1250,7 @@ impl CalendarService { organizer: String, attendees: String, categories: String, - reminder: String, + alarms: Vec, recurrence: String, recurrence_days: Vec, recurrence_interval: u32, @@ -1285,7 +1285,7 @@ impl CalendarService { "organizer": organizer, "attendees": attendees, "categories": categories, - "reminder": reminder, + "alarms": alarms, "recurrence": recurrence, "recurrence_days": recurrence_days, "recurrence_interval": recurrence_interval, @@ -1313,7 +1313,7 @@ impl CalendarService { "organizer": organizer, "attendees": attendees, "categories": categories, - "reminder": reminder, + "alarms": alarms, "recurrence": recurrence, "recurrence_days": recurrence_days, "calendar_path": calendar_path, @@ -1391,7 +1391,7 @@ impl CalendarService { organizer: String, attendees: String, categories: String, - reminder: String, + alarms: Vec, recurrence: String, recurrence_days: Vec, recurrence_interval: u32, @@ -1419,7 +1419,7 @@ impl CalendarService { organizer, attendees, categories, - reminder, + alarms, recurrence, recurrence_days, recurrence_interval, @@ -1450,7 +1450,7 @@ impl CalendarService { organizer: String, attendees: String, categories: String, - reminder: String, + alarms: Vec, recurrence: String, recurrence_days: Vec, recurrence_interval: u32, @@ -1482,7 +1482,7 @@ impl CalendarService { "organizer": organizer, "attendees": attendees, "categories": categories, - "reminder": reminder, + "alarms": alarms, "recurrence": recurrence, "recurrence_days": recurrence_days, "recurrence_interval": recurrence_interval, @@ -1687,7 +1687,7 @@ impl CalendarService { organizer: String, attendees: String, categories: String, - reminder: String, + alarms: Vec, recurrence: String, recurrence_days: Vec, recurrence_interval: u32, @@ -1720,7 +1720,7 @@ impl CalendarService { "organizer": organizer, "attendees": attendees, "categories": categories, - "reminder": reminder, + "alarms": alarms, "recurrence": recurrence, "recurrence_days": recurrence_days, "recurrence_interval": recurrence_interval, diff --git a/frontend/styles.css b/frontend/styles.css index 60ee36a..f010a66 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -1029,8 +1029,6 @@ body { font-weight: 500; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } /* Disable pointer events on existing events when creating a new event */ @@ -1150,11 +1148,41 @@ body { display: flex; flex-direction: column; justify-content: center; + align-items: center; /* Center the content horizontally */ + text-align: center; /* Center text within elements */ pointer-events: auto; z-index: 5; position: relative; } +.week-event .event-title-row { + display: flex; + align-items: center; + justify-content: center; /* Center the title and icon */ + gap: 4px; + width: 100%; + max-width: 100%; +} + +.week-event .event-title { + flex: 1; + min-width: 0 !important; + max-width: calc(100% - 16px) !important; /* This was needed for ellipsis */ + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + font-weight: 600; + margin-bottom: 2px; + display: block !important; /* This was also needed */ + text-align: center; /* Center the text within the title element */ +} + +.week-event .event-reminder-icon { + font-size: 0.6rem; + color: rgba(255, 255, 255, 0.8); + flex-shrink: 0; +} + /* Left-click drag handles */ .resize-handle { position: absolute; @@ -1195,13 +1223,7 @@ body { } -.week-event .event-title { - font-weight: 600; - margin-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} +/* Moved to .week-event .event-header .event-title for better specificity */ .week-event .event-time { font-size: 0.65rem; @@ -1570,9 +1592,6 @@ body { border-radius: 3px; font-size: 0.7rem; line-height: 1.2; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; cursor: pointer; transition: var(--standard-transition); border: 1px solid rgba(255,255,255,0.2); @@ -1580,6 +1599,10 @@ body { font-weight: 500; box-shadow: 0 1px 2px rgba(0,0,0,0.1); position: relative; + display: flex; + align-items: center; + gap: var(--spacing-xs); + min-width: 0; } .event-box:hover { @@ -4429,3 +4452,237 @@ body { border-color: #138496; } +/* Alarm List Component */ +.alarm-list { + margin-bottom: var(--spacing-lg); +} + +.alarm-list h6 { + margin: 0 0 var(--spacing-sm) 0; + font-size: 0.9rem; + font-weight: 600; + color: var(--text-primary); +} + +.alarm-list-empty { + text-align: center; + padding: var(--spacing-lg); + background: var(--background-tertiary); + border: 1px dashed var(--border-secondary); + border-radius: var(--border-radius-medium); + color: var(--text-secondary); +} + +.alarm-list-empty p { + margin: 0; +} + +.alarm-list-empty .text-small { + font-size: 0.8rem; + margin-top: var(--spacing-xs); +} + +.alarm-items { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.alarm-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-sm) var(--spacing-md); + background: var(--background-secondary); + border: 1px solid var(--border-secondary); + border-radius: var(--border-radius-small); + transition: var(--transition-fast); +} + +.alarm-item:hover { + background: var(--background-tertiary); + border-color: var(--border-primary); +} + +.alarm-content { + display: flex; + align-items: center; + gap: var(--spacing-sm); + flex: 1; +} + +.alarm-icon { + font-size: 1.1rem; + min-width: 24px; + text-align: center; +} + +.alarm-description { + font-size: 0.9rem; + color: var(--text-primary); + font-weight: 500; +} + +.alarm-actions { + display: flex; + gap: var(--spacing-xs); +} + +.alarm-action-btn { + background: none; + border: none; + padding: var(--spacing-xs); + border-radius: var(--border-radius-small); + cursor: pointer; + color: var(--text-secondary); + transition: var(--transition-fast); + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; +} + +.alarm-action-btn:hover { + background: var(--background-tertiary); + color: var(--text-primary); +} + +.alarm-action-btn.edit-btn:hover { + background: var(--info-color); + color: white; +} + +.alarm-action-btn.delete-btn:hover { + background: var(--error-color); + color: white; +} + +.alarm-action-btn i { + font-size: 0.8rem; +} + +/* Alarm Management Header */ +.alarm-management-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--spacing-sm); +} + +.alarm-management-header h5 { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); +} + +.add-alarm-button { + background: var(--primary-color); + color: white; + border: none; + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--border-radius-small); + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition-fast); + display: flex; + align-items: center; + gap: var(--spacing-xs); +} + +.add-alarm-button:hover { + background: #1d4ed8; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(29, 78, 216, 0.25); +} + +.add-alarm-button i { + font-size: 0.8rem; +} + +/* Alarm Types Info */ +.alarm-types-info { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-sm); + margin: var(--spacing-md) 0; +} + +.alarm-type-info { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm); + background: var(--background-secondary); + border: 1px solid var(--border-secondary); + border-radius: var(--border-radius-small); + transition: var(--transition-fast); +} + +.alarm-type-info:hover { + background: var(--background-tertiary); + border-color: var(--border-primary); +} + +.alarm-type-info .alarm-icon { + font-size: 1.2rem; + min-width: 24px; + text-align: center; +} + +.alarm-type-info div { + display: flex; + flex-direction: column; + gap: 2px; +} + +.alarm-type-info strong { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-primary); +} + +.alarm-type-info span:not(.alarm-icon) { + font-size: 0.75rem; + color: var(--text-secondary); +} + +/* Event Reminder Icon */ +.event-reminder-icon { + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.8); + margin-left: auto; + flex-shrink: 0; +} + +.event-box .event-title { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.event-content { + display: flex; + align-items: center; + gap: var(--spacing-xs); + width: 100%; + min-width: 0; +} + +.event-content .event-title { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.event-content .event-reminder-icon { + margin-left: auto; + flex-shrink: 0; +} +