Implement accurate time-based event positioning in week view

- Add calculate_event_position() function to convert UTC times to pixel coordinates
- Position events based on actual start/end times with 60px per hour scaling
- Handle timezone conversion from UTC to local time for proper display
- Implement dynamic event heights that stretch from start to end time
- Add special handling for all-day events positioned at top of day columns
- Create enhanced event display with title and formatted time information
- Style all-day events distinctly with gradient background and italic text
- Filter events to show only those belonging to specific dates
- Add CSS styling for event titles, times, and all-day event appearance
- Support minimum 20px height for very short events and multi-day capping

Events now render at their correct times making week view much more useful for scheduling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-29 10:31:09 -04:00
parent dacc18fe5d
commit 5d0628878b
2 changed files with 102 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
use yew::prelude::*;
use chrono::{Datelike, NaiveDate, Duration, Weekday};
use chrono::{Datelike, NaiveDate, Duration, Weekday, Local, Timelike};
use std::collections::HashMap;
use web_sys::MouseEvent;
use crate::services::calendar_service::{CalendarEvent, UserInfo};
@@ -130,10 +130,17 @@ pub fn week_view(props: &WeekViewProps) -> Html {
// Final boundary slot to match the final time label
<div class="time-slot boundary-slot"></div>
// Events positioned absolutely
// Events positioned absolutely based on their actual times
<div class="events-container">
{
day_events.iter().map(|event| {
day_events.iter().filter_map(|event| {
let (start_pixels, duration_pixels, is_all_day) = calculate_event_position(event, *date);
// 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 {
return None;
}
let event_color = get_event_color(event);
let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid);
@@ -158,17 +165,38 @@ pub fn week_view(props: &WeekViewProps) -> Html {
}
};
// For now, position events at top of day (we'll improve this later with actual time positioning)
html! {
// Format time display for the event
let time_display = if event.all_day {
"All Day".to_string()
} else {
let local_start = event.start.with_timezone(&Local);
format!("{}", local_start.format("%I:%M %p"))
};
Some(html! {
<div
class={classes!("week-event", if is_refreshing { Some("refreshing") } else { None })}
style={format!("background-color: {}; top: {}px;", event_color, 60)} // Placeholder positioning
class={classes!(
"week-event",
if is_refreshing { Some("refreshing") } else { None },
if is_all_day { Some("all-day") } else { None }
)}
style={format!(
"background-color: {}; top: {}px; height: {}px;",
event_color,
start_pixels,
duration_pixels
)}
{onclick}
{oncontextmenu}
>
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
{if !is_all_day {
html! { <div class="event-time">{time_display}</div> }
} else {
html! {}
}}
</div>
}
})
}).collect::<Html>()
}
</div>
@@ -207,4 +235,48 @@ fn get_weekday_name(weekday: Weekday) -> &'static str {
Weekday::Fri => "Fri",
Weekday::Sat => "Sat",
}
}
// Calculate the pixel position of an event based on its time
// Each hour is 60px, so we convert time to pixels
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);
let event_date = local_start.date_naive();
// Only position events that are on this specific date
if event_date != date {
return (0.0, 0.0, false); // Event not on this date
}
// Handle all-day events - they appear at the top
if event.all_day {
return (0.0, 30.0, true); // Position at top, 30px height, is_all_day = true
}
// Calculate start position in pixels from midnight
let start_hour = local_start.hour() as f32;
let start_minute = local_start.minute() as f32;
let start_pixels = (start_hour + start_minute / 60.0) * 60.0; // 60px per hour
// Calculate duration and height
let duration_pixels = if let Some(end) = event.end {
let local_end = end.with_timezone(&Local);
let end_date = local_end.date_naive();
// Handle events that span multiple days by capping at midnight
if end_date > date {
// Event continues past midnight, cap at 24:00 (1440px)
1440.0 - start_pixels
} else {
let end_hour = local_end.hour() as f32;
let end_minute = local_end.minute() as f32;
let end_pixels = (end_hour + end_minute / 60.0) * 60.0;
(end_pixels - start_pixels).max(20.0) // Minimum 20px height
}
} else {
60.0 // Default 1 hour if no end time
};
(start_pixels, duration_pixels, false) // is_all_day = false
}

View File

@@ -644,6 +644,27 @@ body {
border-color: #ff9800;
}
.week-event .event-title {
font-weight: 600;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.week-event .event-time {
font-size: 0.65rem;
opacity: 0.9;
font-weight: 400;
}
.week-event.all-day {
opacity: 0.9;
border-left: 4px solid rgba(255,255,255,0.5);
font-style: italic;
background: linear-gradient(135deg, var(--event-color, #3B82F6), rgba(255,255,255,0.1)) !important;
}
/* Legacy Week Grid (for backward compatibility) */
.week-grid {
display: grid;