From 0899a84b429427ab85edc23c7668e3f5d06fb68b Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Tue, 2 Sep 2025 11:13:54 -0400 Subject: [PATCH] Fix all-day events: validation and proper header positioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend fixes: - Fix all-day event creation validation error - Allow same start/end date for all-day events (single-day events) - Maintain strict validation for timed events (end must be after start) Frontend improvements: - Move all-day events from time grid to day headers - Add dedicated all-day events container that stacks vertically - Filter all-day events out of main time-based events area - Add proper CSS styling for all-day event display and interaction - Maintain event click handling and color themes All-day events now appear in the correct location at the top of each day column and properly stack when multiple events exist. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/src/handlers/events.rs | 36 +++++++++++++------ frontend/src/components/week_view.rs | 52 ++++++++++++++++++++++++++-- frontend/styles.css | 44 ++++++++++++++++++++++- 3 files changed, 118 insertions(+), 14 deletions(-) diff --git a/backend/src/handlers/events.rs b/backend/src/handlers/events.rs index 500f99f..ba9ca2d 100644 --- a/backend/src/handlers/events.rs +++ b/backend/src/handlers/events.rs @@ -417,11 +417,19 @@ pub async fn create_event( let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; - // Validate that end is after start - if end_datetime <= start_datetime { - return Err(ApiError::BadRequest( - "End date/time must be after start date/time".to_string(), - )); + // Validate that end is after start (allow equal times for all-day events) + if request.all_day { + if end_datetime < start_datetime { + return Err(ApiError::BadRequest( + "End date must be on or after start date for all-day events".to_string(), + )); + } + } else { + if end_datetime <= start_datetime { + return Err(ApiError::BadRequest( + "End date/time must be after start date/time".to_string(), + )); + } } // Generate a unique UID for the event @@ -707,11 +715,19 @@ pub async fn update_event( let end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; - // Validate that end is after start - if end_datetime <= start_datetime { - return Err(ApiError::BadRequest( - "End date/time must be after start date/time".to_string(), - )); + // Validate that end is after start (allow equal times for all-day events) + if request.all_day { + if end_datetime < start_datetime { + return Err(ApiError::BadRequest( + "End date must be on or after start date for all-day events".to_string(), + )); + } + } else { + if end_datetime <= start_datetime { + return Err(ApiError::BadRequest( + "End date/time must be after start date/time".to_string(), + )); + } } // Update event properties diff --git a/frontend/src/components/week_view.rs b/frontend/src/components/week_view.rs index 6a5f366..cd9c139 100644 --- a/frontend/src/components/week_view.rs +++ b/frontend/src/components/week_view.rs @@ -319,11 +319,52 @@ pub fn week_view(props: &WeekViewProps) -> Html { week_days.iter().map(|date| { let is_today = *date == props.today; let weekday_name = get_weekday_name(date.weekday()); + let day_events = props.events.get(date).cloned().unwrap_or_default(); + + // Filter for all-day events only + let all_day_events: Vec<_> = day_events.iter().filter(|event| event.all_day).collect(); html! {
-
{weekday_name}
-
{date.day()}
+
+
{weekday_name}
+
{date.day()}
+
+ + // All-day events section + {if !all_day_events.is_empty() { + html! { +
+ { + all_day_events.iter().map(|event| { + let event_color = get_event_color(event); + let onclick = { + let on_event_click = props.on_event_click.clone(); + let event = (*event).clone(); + Callback::from(move |e: MouseEvent| { + e.stop_propagation(); + on_event_click.emit(event.clone()); + }) + }; + + html! { +
+ + {event.summary.as_ref().unwrap_or(&"Untitled".to_string())} + +
+ } + }).collect::() + } +
+ } + } else { + html! {} + }}
} }).collect::() @@ -611,8 +652,13 @@ pub fn week_view(props: &WeekViewProps) -> Html { day_events.iter().enumerate().filter_map(|(event_idx, event)| { let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date); + // Skip all-day events (they're rendered in the header) + if is_all_day { + return None; + } + // Skip events that don't belong on this date or have invalid positioning - if start_pixels == 0.0 && duration_pixels == 0.0 && !is_all_day { + if start_pixels == 0.0 && duration_pixels == 0.0 { return None; } diff --git a/frontend/styles.css b/frontend/styles.css index 879bbda..9b4e63a 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -650,11 +650,14 @@ body { } .week-day-header { - padding: 1rem; + padding: 0.5rem; text-align: center; border-right: 1px solid var(--time-label-border, #e9ecef); background: var(--weekday-header-bg, #f8f9fa); color: var(--weekday-header-text, inherit); + min-height: 70px; /* Ensure space for all-day events */ + display: flex; + flex-direction: column; } .week-day-header.today { @@ -680,6 +683,45 @@ body { color: var(--calendar-today-text, #1976d2); } +/* All-day events in header */ +.day-header-content { + flex-shrink: 0; +} + +.all-day-events { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + margin-top: 0.5rem; + min-height: 0; +} + +.all-day-event { + background: #3B82F6; + color: white; + border-radius: 4px; + padding: 2px 6px; + font-size: 0.75rem; + text-align: left; + cursor: pointer; + border: 1px solid rgba(255,255,255,0.2); + min-height: 18px; + display: flex; + align-items: center; +} + +.all-day-event:hover { + filter: brightness(1.1); +} + +.all-day-event-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + /* Week Content */ .week-content { flex: 1;