- Add time range display to week view events showing "start - end" format - Optimize time display with smart AM/PM formatting to reduce redundancy - Fix context menu overlap by adding stop_propagation to event handlers - Implement persistent view mode (Month/Week) across page refreshes using localStorage - Replace month-based tracking with intelligent selected date tracking - Add day selection in month view with visual feedback and click handlers - Fix view switching to navigate to week containing selected day, not first week of month - Separate selected_date from display_date for proper context switching - Simplify week view header to show "Month Year" instead of "Week of Month Day" - Add backward compatibility for existing localStorage keys Greatly improves calendar navigation and user experience with persistent state 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
203 lines
8.8 KiB
Rust
203 lines
8.8 KiB
Rust
use yew::prelude::*;
|
|
use chrono::{Datelike, NaiveDate, Weekday};
|
|
use std::collections::HashMap;
|
|
use web_sys::MouseEvent;
|
|
use crate::services::calendar_service::{CalendarEvent, UserInfo};
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct MonthViewProps {
|
|
pub current_month: NaiveDate,
|
|
pub today: NaiveDate,
|
|
pub events: HashMap<NaiveDate, Vec<CalendarEvent>>,
|
|
pub on_event_click: Callback<CalendarEvent>,
|
|
#[prop_or_default]
|
|
pub refreshing_event_uid: Option<String>,
|
|
#[prop_or_default]
|
|
pub user_info: Option<UserInfo>,
|
|
#[prop_or_default]
|
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
|
#[prop_or_default]
|
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
|
#[prop_or_default]
|
|
pub selected_date: Option<NaiveDate>,
|
|
#[prop_or_default]
|
|
pub on_day_select: Option<Callback<NaiveDate>>,
|
|
}
|
|
|
|
#[function_component(MonthView)]
|
|
pub fn month_view(props: &MonthViewProps) -> Html {
|
|
let first_day_of_month = props.current_month.with_day(1).unwrap();
|
|
let days_in_month = get_days_in_month(props.current_month);
|
|
let first_weekday = first_day_of_month.weekday();
|
|
let days_from_prev_month = get_days_from_previous_month(props.current_month, first_weekday);
|
|
|
|
// Helper function to get calendar color for an event
|
|
let get_event_color = |event: &CalendarEvent| -> String {
|
|
if let Some(user_info) = &props.user_info {
|
|
if let Some(calendar_path) = &event.calendar_path {
|
|
if let Some(calendar) = user_info.calendars.iter()
|
|
.find(|cal| &cal.path == calendar_path) {
|
|
return calendar.color.clone();
|
|
}
|
|
}
|
|
}
|
|
"#3B82F6".to_string()
|
|
};
|
|
|
|
html! {
|
|
<div class="calendar-grid">
|
|
// Weekday headers
|
|
<div class="weekday-header">{"Sun"}</div>
|
|
<div class="weekday-header">{"Mon"}</div>
|
|
<div class="weekday-header">{"Tue"}</div>
|
|
<div class="weekday-header">{"Wed"}</div>
|
|
<div class="weekday-header">{"Thu"}</div>
|
|
<div class="weekday-header">{"Fri"}</div>
|
|
<div class="weekday-header">{"Sat"}</div>
|
|
|
|
// Days from previous month (grayed out)
|
|
{
|
|
days_from_prev_month.iter().map(|day| {
|
|
html! {
|
|
<div class="calendar-day prev-month">{*day}</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
|
|
// Days of the current month
|
|
{
|
|
(1..=days_in_month).map(|day| {
|
|
let date = props.current_month.with_day(day).unwrap();
|
|
let is_today = date == props.today;
|
|
let is_selected = props.selected_date == Some(date);
|
|
let day_events = props.events.get(&date).cloned().unwrap_or_default();
|
|
|
|
html! {
|
|
<div
|
|
class={classes!(
|
|
"calendar-day",
|
|
if is_today { Some("today") } else { None },
|
|
if is_selected { Some("selected") } else { None }
|
|
)}
|
|
onclick={
|
|
if let Some(callback) = &props.on_day_select {
|
|
let callback = callback.clone();
|
|
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.stop_propagation(); // Prevent other handlers
|
|
callback.emit(date);
|
|
}))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
oncontextmenu={
|
|
if let Some(callback) = &props.on_calendar_context_menu {
|
|
let callback = callback.clone();
|
|
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
callback.emit((e, date));
|
|
}))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
>
|
|
<div class="day-number">{day}</div>
|
|
<div class="day-events">
|
|
{
|
|
day_events.iter().map(|event| {
|
|
let event_color = get_event_color(event);
|
|
let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid);
|
|
|
|
let onclick = {
|
|
let on_event_click = props.on_event_click.clone();
|
|
let event = event.clone();
|
|
Callback::from(move |_: MouseEvent| {
|
|
on_event_click.emit(event.clone());
|
|
})
|
|
};
|
|
|
|
let oncontextmenu = {
|
|
if let Some(callback) = &props.on_event_context_menu {
|
|
let callback = callback.clone();
|
|
let event = event.clone();
|
|
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
callback.emit((e, event.clone()));
|
|
}))
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
|
|
html! {
|
|
<div
|
|
class={classes!("event-box", if is_refreshing { Some("refreshing") } else { None })}
|
|
style={format!("background-color: {}", event_color)}
|
|
{onclick}
|
|
{oncontextmenu}
|
|
>
|
|
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
|
|
</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
|
|
{ render_next_month_days(days_from_prev_month.len(), days_in_month) }
|
|
</div>
|
|
}
|
|
}
|
|
|
|
fn render_next_month_days(prev_days_count: usize, current_days_count: u32) -> Html {
|
|
let total_slots = 42; // 6 rows x 7 days
|
|
let used_slots = prev_days_count + current_days_count as usize;
|
|
let remaining_slots = if used_slots < total_slots { total_slots - used_slots } else { 0 };
|
|
|
|
(1..=remaining_slots).map(|day| {
|
|
html! {
|
|
<div class="calendar-day next-month">{day}</div>
|
|
}
|
|
}).collect::<Html>()
|
|
}
|
|
|
|
fn get_days_in_month(date: NaiveDate) -> u32 {
|
|
NaiveDate::from_ymd_opt(
|
|
if date.month() == 12 { date.year() + 1 } else { date.year() },
|
|
if date.month() == 12 { 1 } else { date.month() + 1 },
|
|
1
|
|
)
|
|
.unwrap()
|
|
.pred_opt()
|
|
.unwrap()
|
|
.day()
|
|
}
|
|
|
|
fn get_days_from_previous_month(current_month: NaiveDate, first_weekday: Weekday) -> Vec<u32> {
|
|
let days_before = match first_weekday {
|
|
Weekday::Sun => 0,
|
|
Weekday::Mon => 1,
|
|
Weekday::Tue => 2,
|
|
Weekday::Wed => 3,
|
|
Weekday::Thu => 4,
|
|
Weekday::Fri => 5,
|
|
Weekday::Sat => 6,
|
|
};
|
|
|
|
if days_before == 0 {
|
|
vec![]
|
|
} else {
|
|
let prev_month = if current_month.month() == 1 {
|
|
NaiveDate::from_ymd_opt(current_month.year() - 1, 12, 1).unwrap()
|
|
} else {
|
|
NaiveDate::from_ymd_opt(current_month.year(), current_month.month() - 1, 1).unwrap()
|
|
};
|
|
|
|
let prev_month_days = get_days_in_month(prev_month);
|
|
((prev_month_days - days_before as u32 + 1)..=prev_month_days).collect()
|
|
}
|
|
} |