use yew::prelude::*; use chrono::{Datelike, Local, NaiveDate, Duration, Weekday}; use std::collections::HashMap; use crate::services::calendar_service::{CalendarEvent, UserInfo}; use crate::components::EventModal; use wasm_bindgen::JsCast; #[derive(Properties, PartialEq)] pub struct CalendarProps { #[prop_or_default] 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>, } #[function_component] pub fn Calendar(props: &CalendarProps) -> Html { let today = Local::now().date_naive(); let current_month = use_state(|| today); let selected_day = use_state(|| today); let selected_event = use_state(|| None::); // 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 { // Find the calendar that matches this event's path if let Some(calendar) = user_info.calendars.iter() .find(|cal| &cal.path == calendar_path) { return calendar.color.clone(); } } } // Default color if no match found "#3B82F6".to_string() }; let first_day_of_month = current_month.with_day(1).unwrap(); let days_in_month = get_days_in_month(*current_month); let first_weekday = first_day_of_month.weekday(); let days_from_prev_month = get_days_from_previous_month(*current_month, first_weekday); let prev_month = { let current_month = current_month.clone(); Callback::from(move |_| { let prev = *current_month - Duration::days(1); let first_of_prev = prev.with_day(1).unwrap(); current_month.set(first_of_prev); }) }; let next_month = { let current_month = current_month.clone(); Callback::from(move |_| { let next = if current_month.month() == 12 { NaiveDate::from_ymd_opt(current_month.year() + 1, 1, 1).unwrap() } else { NaiveDate::from_ymd_opt(current_month.year(), current_month.month() + 1, 1).unwrap() }; current_month.set(next); }) }; html! {

{format!("{} {}", get_month_name(current_month.month()), current_month.year())}

// 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 current month { (1..=days_in_month).map(|day| { let date = current_month.with_day(day).unwrap(); let is_today = date == today; let is_selected = date == *selected_day; let events = props.events.get(&date).cloned().unwrap_or_default(); let mut classes = vec!["calendar-day", "current-month"]; if is_today { classes.push("today"); } if is_selected { classes.push("selected"); } if !events.is_empty() { classes.push("has-events"); } let selected_day_clone = selected_day.clone(); let on_click = Callback::from(move |_| { selected_day_clone.set(date); }); let on_context_menu = { let on_calendar_context_menu = props.on_calendar_context_menu.clone(); Callback::from(move |e: MouseEvent| { // Only show context menu if we're not right-clicking on an event if let Some(target) = e.target() { if let Ok(element) = target.dyn_into::() { // Check if the click is on an event box or inside one let mut current = Some(element); while let Some(el) = current { if el.class_name().contains("event-box") { return; // Don't show calendar context menu on events } current = el.parent_element(); } } } e.prevent_default(); e.stop_propagation(); if let Some(callback) = &on_calendar_context_menu { callback.emit((e, date)); } }) }; html! {
{day}
{ if !events.is_empty() { html! {
{ events.iter().take(2).map(|event| { let event_clone = event.clone(); let selected_event_clone = selected_event.clone(); let on_event_click = props.on_event_click.clone(); let event_click = Callback::from(move |e: MouseEvent| { e.stop_propagation(); // Prevent day selection on_event_click.emit(event_clone.clone()); selected_event_clone.set(Some(event_clone.clone())); }); let event_context_menu = { let event_clone = event.clone(); let on_event_context_menu = props.on_event_context_menu.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); e.stop_propagation(); if let Some(callback) = &on_event_context_menu { callback.emit((e, event_clone.clone())); } }) }; let title = event.get_title(); let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid); let class_name = if is_refreshing { "event-box refreshing" } else { "event-box" }; let event_color = get_event_color(&event); html! {
{ if is_refreshing { "🔄 Refreshing...".to_string() } else if title.len() > 15 { format!("{}...", &title[..12]) } else { title } }
} }).collect::() } { if events.len() > 2 { html! {
{format!("+{} more", events.len() - 2)}
} } else { html! {} } }
} } else { html! {} } }
} }).collect::() } { render_next_month_days(days_from_prev_month.len(), days_in_month) }
// Event details modal
} } 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 { // Calculate the previous month 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() } } fn get_month_name(month: u32) -> &'static str { match month { 1 => "January", 2 => "February", 3 => "March", 4 => "April", 5 => "May", 6 => "June", 7 => "July", 8 => "August", 9 => "September", 10 => "October", 11 => "November", 12 => "December", _ => "Invalid" } }