diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 7cfd09c..e6869f6 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -66,6 +66,20 @@ pub fn Calendar(props: &CalendarProps) -> Html { let show_create_modal = use_state(|| false); let create_event_data = use_state(|| None::<(chrono::NaiveDate, chrono::NaiveTime, chrono::NaiveTime)>); + // State for time increment snapping (15 or 30 minutes) + let time_increment = use_state(|| { + // Try to load saved time increment from localStorage + if let Ok(saved_increment) = LocalStorage::get::("calendar_time_increment") { + if saved_increment == 15 || saved_increment == 30 { + saved_increment + } else { + 15 + } + } else { + 15 + } + }); + // Handle view mode changes - adjust current_date format when switching between month/week { let current_date = current_date.clone(); @@ -152,6 +166,17 @@ pub fn Calendar(props: &CalendarProps) -> Html { }) }; + // Handle time increment toggle + let on_time_increment_toggle = { + let time_increment = time_increment.clone(); + Callback::from(move |_: MouseEvent| { + let current = *time_increment; + let next = if current == 15 { 30 } else { 15 }; + time_increment.set(next); + let _ = LocalStorage::set("calendar_time_increment", next); + }) + }; + // Handle drag-to-create event let on_create_event = { let show_create_modal = show_create_modal.clone(); @@ -172,6 +197,8 @@ pub fn Calendar(props: &CalendarProps) -> Html { on_prev={on_prev} on_next={on_next} on_today={on_today} + time_increment={Some(*time_increment)} + on_time_increment_toggle={Some(on_time_increment_toggle)} /> { @@ -212,6 +239,7 @@ pub fn Calendar(props: &CalendarProps) -> Html { on_calendar_context_menu={props.on_calendar_context_menu.clone()} on_create_event={Some(on_create_event)} context_menus_open={props.context_menus_open} + time_increment={*time_increment} /> }, } diff --git a/src/components/calendar_header.rs b/src/components/calendar_header.rs index f64ae0f..a99089f 100644 --- a/src/components/calendar_header.rs +++ b/src/components/calendar_header.rs @@ -10,6 +10,10 @@ pub struct CalendarHeaderProps { pub on_prev: Callback, pub on_next: Callback, pub on_today: Callback, + #[prop_or_default] + pub time_increment: Option, + #[prop_or_default] + pub on_time_increment_toggle: Option>, } #[function_component(CalendarHeader)] @@ -18,7 +22,20 @@ pub fn calendar_header(props: &CalendarHeaderProps) -> Html { html! {
- +
+ + { + if let (Some(increment), Some(callback)) = (props.time_increment, &props.on_time_increment_toggle) { + html! { + + } + } else { + html! {} + } + } +

{title}

diff --git a/src/components/week_view.rs b/src/components/week_view.rs index f2fc4d4..e5bab7e 100644 --- a/src/components/week_view.rs +++ b/src/components/week_view.rs @@ -22,6 +22,8 @@ pub struct WeekViewProps { pub on_create_event: Option>, #[prop_or_default] pub context_menus_open: bool, + #[prop_or_default] + pub time_increment: u32, } #[derive(Clone, PartialEq)] @@ -122,6 +124,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { let onmousedown = { let drag_state = drag_state_clone.clone(); let context_menus_open = props.context_menus_open; + let time_increment = props.time_increment; Callback::from(move |e: MouseEvent| { // Don't start drag if any context menu is open if context_menus_open { @@ -138,8 +141,8 @@ pub fn week_view(props: &WeekViewProps) -> Html { let relative_y = e.layer_y() as f64; let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; - // Snap to 15-minute increments - let snapped_y = snap_to_15_minutes(relative_y); + // Snap to increment + let snapped_y = snap_to_increment(relative_y, time_increment); drag_state.set(Some(DragState { is_dragging: true, @@ -153,6 +156,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { let onmousemove = { let drag_state = drag_state_clone.clone(); + let time_increment = props.time_increment; Callback::from(move |e: MouseEvent| { if let Some(mut current_drag) = (*drag_state).clone() { if current_drag.is_dragging { @@ -160,8 +164,8 @@ pub fn week_view(props: &WeekViewProps) -> Html { let relative_y = e.layer_y() as f64; let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; - // Snap to 15-minute increments - let snapped_y = snap_to_15_minutes(relative_y); + // Snap to increment + let snapped_y = snap_to_increment(relative_y, time_increment); current_drag.current_y = snapped_y; drag_state.set(Some(current_drag)); @@ -214,7 +218,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { {onmousemove} {onmouseup} > - // Time slot backgrounds - 24 full hour slots + 1 boundary slot + // Time slot backgrounds - 24 hour slots to represent full day { (0..24).map(|_hour| { html! { @@ -225,8 +229,11 @@ pub fn week_view(props: &WeekViewProps) -> Html { } }).collect::() } - // Final boundary slot to match the final time label -
+ // Final boundary slot to complete the 24-hour visual grid - make it interactive like other slots +
+
+
+
// Events positioned absolutely based on their actual times
@@ -398,9 +405,9 @@ fn get_weekday_name(weekday: Weekday) -> &'static str { // Calculate the pixel position of an event based on its time // Each hour is 60px, so we convert time to pixels // Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes) -fn snap_to_15_minutes(pixels: f64) -> f64 { - let increment = 15.0; // 15px = 15 minutes - (pixels / increment).round() * increment +fn snap_to_increment(pixels: f64, increment: u32) -> f64 { + let increment_px = increment as f64; // Convert to pixels (1px = 1 minute) + (pixels / increment_px).round() * increment_px } // Convert pixel position to time (inverse of time to pixels) @@ -410,7 +417,12 @@ fn pixels_to_time(pixels: f64) -> NaiveTime { let hours = (total_minutes / 60.0) as u32; let minutes = (total_minutes % 60.0) as u32; - // Clamp to valid time range + // Handle midnight boundary - if we're at exactly 1440 pixels (24:00), return midnight + if total_minutes >= 1440.0 { + return NaiveTime::from_hms_opt(0, 0, 0).unwrap(); + } + + // Clamp to valid time range for within-day times let hours = hours.min(23); let minutes = minutes.min(59); diff --git a/styles.css b/styles.css index b5bb58a..ddb0156 100644 --- a/styles.css +++ b/styles.css @@ -410,12 +410,38 @@ body { transform: translateX(-50%); } +.header-left { + display: flex; + align-items: center; + gap: 8px; +} + .header-right { display: flex; align-items: center; gap: 0.5rem; } +.time-increment-button { + background: rgba(255,255,255,0.2); + border: none; + color: white; + font-size: 14px; + font-weight: bold; + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; +} + +.time-increment-button:hover { + background: rgba(255,255,255,0.3); +} + .nav-button { background: rgba(255,255,255,0.2); border: none;