use crate::models::ical::VEvent; use crate::services::calendar_service::UserInfo; use chrono::{Datelike, NaiveDate, Weekday}; use std::collections::HashMap; use wasm_bindgen::{prelude::*, JsCast}; use web_sys::window; use yew::prelude::*; #[derive(Properties, PartialEq)] pub struct MonthViewProps { pub current_month: NaiveDate, pub today: NaiveDate, pub events: HashMap>, pub on_event_click: Callback, #[prop_or_default] pub refreshing_event_uid: Option, #[prop_or_default] pub user_info: Option, #[prop_or_default] pub on_event_context_menu: Option>, #[prop_or_default] pub on_calendar_context_menu: Option>, #[prop_or_default] pub selected_date: Option, #[prop_or_default] pub on_day_select: Option>, } #[function_component(MonthView)] pub fn month_view(props: &MonthViewProps) -> Html { let max_events_per_day = use_state(|| 4); // Default to 4 events max 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); // Calculate maximum events that can fit based on available height let calculate_max_events = { let max_events_per_day = max_events_per_day.clone(); move || { // Since we're using CSS Grid with equal row heights, // we can estimate based on typical calendar dimensions // Typical calendar height is around 600-800px for 6 rows // Each row gets ~100-133px, minus day number and padding leaves ~70-100px // Each event is ~18px, so we can fit ~3-4 events + "+n more" indicator max_events_per_day.set(3); } }; // Setup resize handler and initial calculation { let calculate_max_events = calculate_max_events.clone(); use_effect_with((), move |_| { let calculate_max_events_clone = calculate_max_events.clone(); // Initial calculation with a slight delay to ensure DOM is ready if let Some(window) = window() { let timeout_closure = Closure::wrap(Box::new(move || { calculate_max_events_clone(); }) as Box); let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0( timeout_closure.as_ref().unchecked_ref(), 100, ); timeout_closure.forget(); } // Setup resize listener let resize_closure = Closure::wrap(Box::new(move || { calculate_max_events(); }) as Box); if let Some(window) = window() { let _ = window.add_event_listener_with_callback( "resize", resize_closure.as_ref().unchecked_ref(), ); resize_closure.forget(); // Keep the closure alive } || {} }); } // Helper function to get calendar color for an event let get_event_color = |event: &VEvent| -> 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! {
// Weekday headers
{"Sun"}
{"Mon"}
{"Tue"}
{"Wed"}
{"Thu"}
{"Fri"}
{"Sat"}
// Days from previous month (grayed out) { days_from_prev_month.iter().map(|day| { html! {
{*day}
} }).collect::() } // 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(); // Calculate visible events and overflow let max_events = *max_events_per_day as usize; let visible_events: Vec<_> = day_events.iter().take(max_events).collect(); let hidden_count = day_events.len().saturating_sub(max_events); html! {
{day}
{ visible_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 |_: web_sys::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! {
{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}
} }).collect::() } { if hidden_count > 0 { html! {
{format!("+{} more", hidden_count)}
} } else { html! {} } }
} }).collect::() } { render_next_month_days(days_from_prev_month.len(), days_in_month) }
} } 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! {
{day}
} }) .collect::() } 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 { 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() } }