diff --git a/src/components/week_view.rs b/src/components/week_view.rs index 5c3d759..d240157 100644 --- a/src/components/week_view.rs +++ b/src/components/week_view.rs @@ -1,7 +1,8 @@ use yew::prelude::*; -use chrono::{Datelike, NaiveDate, Duration, Weekday, Local, Timelike}; +use chrono::{Datelike, NaiveDate, Duration, Weekday, Local, Timelike, NaiveDateTime, NaiveTime}; use std::collections::HashMap; use web_sys::MouseEvent; +use wasm_bindgen::JsCast; use crate::services::calendar_service::{CalendarEvent, UserInfo}; #[derive(Properties, PartialEq)] @@ -18,6 +19,16 @@ pub struct WeekViewProps { pub on_event_context_menu: Option>, #[prop_or_default] pub on_calendar_context_menu: Option>, + #[prop_or_default] + pub on_create_event: Option>, +} + +#[derive(Clone, PartialEq)] +struct DragState { + is_dragging: bool, + start_date: NaiveDate, + start_y: f64, + current_y: f64, } #[function_component(WeekView)] @@ -27,6 +38,9 @@ pub fn week_view(props: &WeekViewProps) -> Html { .map(|i| start_of_week + Duration::days(i)) .collect(); + // Drag state for event creation + let drag_state = use_state(|| None::); + // Helper function to get calendar color for an event let get_event_color = |event: &CalendarEvent| -> String { if let Some(user_info) = &props.user_info { @@ -96,10 +110,85 @@ pub fn week_view(props: &WeekViewProps) -> Html { // Day columns
{ - week_days.iter().map(|date| { + week_days.iter().enumerate().map(|(_column_index, date)| { let is_today = *date == props.today; let day_events = props.events.get(date).cloned().unwrap_or_default(); + // Drag event handlers + let drag_state_clone = drag_state.clone(); + let date_for_drag = *date; + + let onmousedown = { + let drag_state = drag_state_clone.clone(); + Callback::from(move |e: MouseEvent| { + // Calculate Y position relative to day column container + // Use layer_y which gives coordinates relative to positioned ancestor + let relative_y = e.layer_y() as f64; + let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; + + drag_state.set(Some(DragState { + is_dragging: true, + start_date: date_for_drag, + start_y: relative_y, + current_y: relative_y, + })); + e.prevent_default(); + }) + }; + + let onmousemove = { + let drag_state = drag_state_clone.clone(); + Callback::from(move |e: MouseEvent| { + if let Some(mut current_drag) = (*drag_state).clone() { + if current_drag.is_dragging { + // Use layer_y for consistent coordinate calculation + let relative_y = e.layer_y() as f64; + let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 }; + + current_drag.current_y = relative_y; + drag_state.set(Some(current_drag)); + } + } + }) + }; + + let onmouseup = { + let drag_state = drag_state_clone.clone(); + let on_create_event = props.on_create_event.clone(); + Callback::from(move |_e: MouseEvent| { + if let Some(current_drag) = (*drag_state).clone() { + if current_drag.is_dragging { + // Calculate start and end times + let start_time = pixels_to_time(current_drag.start_y); + let end_time = pixels_to_time(current_drag.current_y); + + // Ensure start is before end + let (actual_start, actual_end) = if start_time <= end_time { + (start_time, end_time) + } else { + (end_time, start_time) + }; + + // Ensure minimum duration (15 minutes) + let actual_end = if actual_end.signed_duration_since(actual_start).num_minutes() < 15 { + actual_start + chrono::Duration::minutes(15) + } else { + actual_end + }; + + let start_datetime = NaiveDateTime::new(current_drag.start_date, actual_start); + let end_datetime = NaiveDateTime::new(current_drag.start_date, actual_end); + + if let Some(callback) = &on_create_event { + callback.emit((current_drag.start_date, start_datetime, end_datetime)); + } + + drag_state.set(None); + } + } + }) + }; + html! {
Html { None } } + {onmousedown} + {onmousemove} + {onmouseup} > // Time slot backgrounds - 24 full hour slots + 1 boundary slot { @@ -225,6 +317,31 @@ pub fn week_view(props: &WeekViewProps) -> Html { }).collect::() }
+ + // Temporary event box during drag + { + if let Some(drag) = (*drag_state).clone() { + if drag.is_dragging && drag.start_date == *date { + let start_y = drag.start_y.min(drag.current_y); + let height = (drag.current_y - drag.start_y).abs().max(20.0); + + // Debug logging + + html! { +
+ {format!("{}px - {}px", start_y as u32, (start_y + height) as u32)} +
+ } + } else { + html! {} + } + } else { + html! {} + } + }
} }).collect::() @@ -264,6 +381,20 @@ 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 +// Convert pixel position to time (inverse of time to pixels) +fn pixels_to_time(pixels: f64) -> NaiveTime { + let total_minutes = (pixels / 60.0) * 60.0; // 60px per hour, 60 minutes per hour + let hours = (total_minutes / 60.0) as u32; + let minutes = (total_minutes % 60.0) as u32; + + // Clamp to valid time range + let hours = hours.min(23); + let minutes = minutes.min(59); + + NaiveTime::from_hms_opt(hours, minutes, 0).unwrap_or(NaiveTime::from_hms_opt(0, 0, 0).unwrap()) +} + + fn calculate_event_position(event: &CalendarEvent, date: NaiveDate) -> (f32, f32, bool) { // Convert UTC times to local time for display let local_start = event.start.with_timezone(&Local); diff --git a/styles.css b/styles.css index 6cdf730..b5bb58a 100644 --- a/styles.css +++ b/styles.css @@ -582,11 +582,13 @@ body { height: 60px; border-bottom: 1px solid #f0f0f0; position: relative; + pointer-events: none; /* Don't capture mouse events */ } .time-slot-half { height: 30px; border-bottom: 1px dotted #f5f5f5; + pointer-events: none; /* Don't capture mouse events */ } .time-slot-half:last-child { @@ -597,6 +599,7 @@ body { height: 60px; /* Match the final time label height */ border-bottom: 2px solid #e9ecef; /* Strong border to match final boundary */ background: rgba(0,0,0,0.02); /* Slightly different background to indicate boundary */ + pointer-events: none; /* Don't capture mouse events */ } /* Events Container */ @@ -606,7 +609,7 @@ body { left: 0; right: 0; bottom: 0; - pointer-events: none; + pointer-events: none; /* Container doesn't capture, but children (events) do */ } /* Week Events */ @@ -644,6 +647,27 @@ body { border-color: #ff9800; } +/* Temporary event box during drag creation */ +.temp-event-box { + position: absolute; + left: 4px; + right: 4px; + background: rgba(59, 130, 246, 0.3); + border: 2px dashed rgba(59, 130, 246, 0.8); + border-radius: 4px; + color: rgba(59, 130, 246, 0.9); + font-size: 0.75rem; + font-weight: 600; + padding: 4px 6px; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + z-index: 6; /* Higher than events */ + text-align: center; + user-select: none; +} + .week-event .event-title { font-weight: 600; margin-bottom: 2px;