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:
@@ -1,5 +1,5 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use chrono::{Datelike, NaiveDate, Duration, Weekday};
|
use chrono::{Datelike, NaiveDate, Duration, Weekday, Local, Timelike};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use web_sys::MouseEvent;
|
use web_sys::MouseEvent;
|
||||||
use crate::services::calendar_service::{CalendarEvent, UserInfo};
|
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
|
// Final boundary slot to match the final time label
|
||||||
<div class="time-slot boundary-slot"></div>
|
<div class="time-slot boundary-slot"></div>
|
||||||
|
|
||||||
// Events positioned absolutely
|
// Events positioned absolutely based on their actual times
|
||||||
<div class="events-container">
|
<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 event_color = get_event_color(event);
|
||||||
let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid);
|
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)
|
// Format time display for the event
|
||||||
html! {
|
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
|
<div
|
||||||
class={classes!("week-event", if is_refreshing { Some("refreshing") } else { None })}
|
class={classes!(
|
||||||
style={format!("background-color: {}; top: {}px;", event_color, 60)} // Placeholder positioning
|
"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}
|
{onclick}
|
||||||
{oncontextmenu}
|
{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>
|
</div>
|
||||||
}
|
})
|
||||||
}).collect::<Html>()
|
}).collect::<Html>()
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -208,3 +236,47 @@ fn get_weekday_name(weekday: Weekday) -> &'static str {
|
|||||||
Weekday::Sat => "Sat",
|
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
|
||||||
|
}
|
||||||
21
styles.css
21
styles.css
@@ -644,6 +644,27 @@ body {
|
|||||||
border-color: #ff9800;
|
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) */
|
/* Legacy Week Grid (for backward compatibility) */
|
||||||
.week-grid {
|
.week-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user