diff --git a/src/components/week_view.rs b/src/components/week_view.rs index 326de39..9afbd8b 100644 --- a/src/components/week_view.rs +++ b/src/components/week_view.rs @@ -32,6 +32,8 @@ pub struct WeekViewProps { enum DragType { CreateEvent, MoveEvent(CalendarEvent), + ResizeEventStart(CalendarEvent), // Resizing from the top edge (start time) + ResizeEventEnd(CalendarEvent), // Resizing from the bottom edge (end time) } #[derive(Clone, PartialEq)] @@ -234,6 +236,51 @@ pub fn week_view(props: &WeekViewProps) -> Html { let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time); let new_end_datetime = new_start_datetime + original_duration; + if let Some(callback) = &on_event_update { + callback.emit((event.clone(), new_start_datetime, new_end_datetime)); + } + }, + DragType::ResizeEventStart(event) => { + // Calculate new start time based on drag position + let new_start_time = pixels_to_time(current_drag.current_y); + + // Keep the original end time + let original_end = if let Some(end) = event.end { + end.with_timezone(&chrono::Local).naive_local() + } else { + // If no end time, use start time + 1 hour as default + event.start.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1) + }; + + let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time); + + // Ensure start is before end (minimum 15 minutes) + let new_end_datetime = if new_start_datetime >= original_end { + new_start_datetime + chrono::Duration::minutes(15) + } else { + original_end + }; + + if let Some(callback) = &on_event_update { + callback.emit((event.clone(), new_start_datetime, new_end_datetime)); + } + }, + DragType::ResizeEventEnd(event) => { + // Calculate new end time based on drag position + let new_end_time = pixels_to_time(current_drag.current_y); + + // Keep the original start time + let original_start = event.start.with_timezone(&chrono::Local).naive_local(); + + let new_end_datetime = NaiveDateTime::new(current_drag.start_date, new_end_time); + + // Ensure end is after start (minimum 15 minutes) + let new_start_datetime = if new_end_datetime <= original_start { + new_end_datetime - chrono::Duration::minutes(15) + } else { + original_start + }; + if let Some(callback) = &on_event_update { callback.emit((event.clone(), new_start_datetime, new_end_datetime)); } @@ -301,24 +348,22 @@ pub fn week_view(props: &WeekViewProps) -> Html { Callback::from(move |e: MouseEvent| { e.stop_propagation(); // Prevent drag-to-create from starting on event clicks - // Only handle left-click (button 0) + // Only handle left-click (button 0) for moving if e.button() != 0 { return; } - // Calculate Y position relative to the day column - let relative_y = e.layer_y() as f64; - let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; + // Calculate absolute Y position for drag calculations + let absolute_y = e.layer_y() as f64; + let absolute_y = if absolute_y > 0.0 { absolute_y } else { e.offset_y() as f64 }; - // Get event's current position + // Get event's current position for offset calculation let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag); let event_start_pixels = event_start_pixels as f64; - - // Calculate offset from the top of the event - let offset_y = relative_y - event_start_pixels; + let offset_y = absolute_y - event_start_pixels; // Snap to increment - let snapped_y = snap_to_increment(relative_y, time_increment); + let snapped_y = snap_to_increment(absolute_y, time_increment); drag_state.set(Some(DragState { is_dragging: true, @@ -336,7 +381,16 @@ pub fn week_view(props: &WeekViewProps) -> Html { if let Some(callback) = &props.on_event_context_menu { let callback = callback.clone(); let event = event.clone(); + let drag_state_for_context = drag_state.clone(); Some(Callback::from(move |e: web_sys::MouseEvent| { + // Check if we're currently dragging - if so, prevent context menu + if let Some(drag) = (*drag_state_for_context).clone() { + if drag.is_dragging { + e.prevent_default(); + return; + } + } + e.prevent_default(); e.stop_propagation(); // Prevent calendar context menu from also triggering callback.emit((e, event.clone())); @@ -377,12 +431,16 @@ pub fn week_view(props: &WeekViewProps) -> Html { } }; - // Check if this event is currently being dragged + // Check if this event is currently being dragged or resized let is_being_dragged = if let Some(drag) = (*drag_state).clone() { - if let DragType::MoveEvent(dragged_event) = &drag.drag_type { - dragged_event.uid == event.uid && drag.is_dragging - } else { - false + match &drag.drag_type { + DragType::MoveEvent(dragged_event) => + dragged_event.uid == event.uid && drag.is_dragging, + DragType::ResizeEventStart(dragged_event) => + dragged_event.uid == event.uid && drag.is_dragging, + DragType::ResizeEventEnd(dragged_event) => + dragged_event.uid == event.uid && drag.is_dragging, + _ => false, } } else { false @@ -392,6 +450,55 @@ pub fn week_view(props: &WeekViewProps) -> Html { // Hide the original event while being dragged Some(html! {}) } else { + // Create resize handles for left-click resize + let resize_start_handler = { + let drag_state = drag_state.clone(); + let event_for_resize = event.clone(); + let date_for_drag = *date; + let time_increment = props.time_increment; + Callback::from(move |e: web_sys::MouseEvent| { + e.stop_propagation(); + + let relative_y = e.layer_y() as f64; + let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; + let snapped_y = snap_to_increment(relative_y, time_increment); + + drag_state.set(Some(DragState { + is_dragging: true, + drag_type: DragType::ResizeEventStart(event_for_resize.clone()), + start_date: date_for_drag, + start_y: snapped_y, + current_y: snapped_y, + offset_y: 0.0, + })); + e.prevent_default(); + }) + }; + + let resize_end_handler = { + let drag_state = drag_state.clone(); + let event_for_resize = event.clone(); + let date_for_drag = *date; + let time_increment = props.time_increment; + Callback::from(move |e: web_sys::MouseEvent| { + e.stop_propagation(); + + let relative_y = e.layer_y() as f64; + let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; + let snapped_y = snap_to_increment(relative_y, time_increment); + + drag_state.set(Some(DragState { + is_dragging: true, + drag_type: DragType::ResizeEventEnd(event_for_resize.clone()), + start_date: date_for_drag, + start_y: snapped_y, + current_y: snapped_y, + offset_y: 0.0, + })); + e.prevent_default(); + }) + }; + Some(html! {
Html { {oncontextmenu} onmousedown={onmousedown_event} > -
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
+ // Top resize handle {if !is_all_day { - html! {
{time_display}
} + html! { +
+ } + } else { + html! {} + }} + + // Event content +
+
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
+ {if !is_all_day { + html! {
{time_display}
} + } else { + html! {} + }} +
+ + // Bottom resize handle + {if !is_all_day { + html! { +
+ } } else { html! {} }} @@ -467,6 +601,58 @@ pub fn week_view(props: &WeekViewProps) -> Html {
{format!("{} - {}", new_start_time.format("%I:%M %p"), new_end_time.format("%I:%M %p"))}
} + }, + DragType::ResizeEventStart(event) => { + // Show the event being resized from the start + let new_start_time = pixels_to_time(drag.current_y); + let original_end = if let Some(end) = event.end { + end.with_timezone(&chrono::Local).naive_local() + } else { + event.start.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1) + }; + + // Calculate positions for the preview + let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date); + let original_duration = original_end.signed_duration_since(event.start.with_timezone(&chrono::Local).naive_local()); + let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32); + + let new_start_pixels = drag.current_y; + let new_height = (original_end_pixels as f64 - new_start_pixels).max(20.0); + + let event_color = get_event_color(event); + + html! { +
+
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
+
{format!("{} - {}", new_start_time.format("%I:%M %p"), original_end.time().format("%I:%M %p"))}
+
+ } + }, + DragType::ResizeEventEnd(event) => { + // Show the event being resized from the end + let new_end_time = pixels_to_time(drag.current_y); + let original_start = event.start.with_timezone(&chrono::Local).naive_local(); + + // Calculate positions for the preview + let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date); + + let new_end_pixels = drag.current_y; + let new_height = (new_end_pixels - original_start_pixels as f64).max(20.0); + + let event_color = get_event_color(event); + + html! { +
+
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
+
{format!("{} - {}", original_start.time().format("%I:%M %p"), new_end_time.format("%I:%M %p"))}
+
+ } } } } else { diff --git a/styles.css b/styles.css index 073128b..d8cfdb3 100644 --- a/styles.css +++ b/styles.css @@ -727,6 +727,93 @@ body { width: 100%; } +/* Resizing event during drag */ +.temp-event-box.resizing-event { + background: inherit; + border: 2px solid rgba(255, 255, 255, 0.9); + color: white; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + text-align: left; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + transform: scale(1.01); +} + +.temp-event-box.resizing-event .event-title { + font-weight: 600; + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +.temp-event-box.resizing-event .event-time { + font-size: 0.65rem; + opacity: 0.9; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +/* Event resize zones and handles */ +.week-event { + position: relative; +} + +.week-event .event-content { + padding: 2px; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + pointer-events: auto; + z-index: 5; + position: relative; +} + +/* Left-click drag handles */ +.resize-handle { + position: absolute; + left: 0; + right: 0; + height: 4px; + background: transparent; + cursor: ns-resize; + z-index: 10; + transition: background-color 0.2s; +} + +.resize-handle-top { + top: 0; + border-top: 2px solid transparent; +} + +.resize-handle-bottom { + bottom: 0; + border-bottom: 2px solid transparent; +} + +.week-event:hover .resize-handle { + background: rgba(255, 255, 255, 0.3); +} + +.week-event:hover .resize-handle-top { + border-top-color: rgba(255, 255, 255, 0.8); +} + +.week-event:hover .resize-handle-bottom { + border-bottom-color: rgba(255, 255, 255, 0.8); +} + +.resize-handle:hover { + background: rgba(255, 255, 255, 0.5) !important; + border-color: rgba(255, 255, 255, 1) !important; +} + + .week-event .event-title { font-weight: 600; margin-bottom: 2px;