 5c966b2571
			
		
	
	5c966b2571
	
	
	
		
			
			- Add CalendarContextMenu component for right-click on calendar days - Add CreateEventModal component with comprehensive event creation form - Integrate context menu detection to avoid conflicts between event/calendar menus - Add form validation and date/time selection with all-day toggle - Connect modal through component hierarchy from app to calendar 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			303 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			303 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| 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<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)>>,
 | ||
| }
 | ||
| 
 | ||
| #[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::<CalendarEvent>);
 | ||
|     
 | ||
|     // 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! {
 | ||
|         <div class="calendar">
 | ||
|             <div class="calendar-header">
 | ||
|                 <button class="nav-button" onclick={prev_month}>{"‹"}</button>
 | ||
|                 <h2 class="month-year">{format!("{} {}", get_month_name(current_month.month()), current_month.year())}</h2>
 | ||
|                 <button class="nav-button" onclick={next_month}>{"›"}</button>
 | ||
|             </div>
 | ||
|             
 | ||
|             <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 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::<web_sys::Element>() {
 | ||
|                                         // 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! {
 | ||
|                             <div class={classes!(classes)} onclick={on_click} oncontextmenu={on_context_menu}>
 | ||
|                                 <div class="day-number">{day}</div>
 | ||
|                                 {
 | ||
|                                     if !events.is_empty() {
 | ||
|                                         html! {
 | ||
|                                             <div class="event-indicators">
 | ||
|                                                 {
 | ||
|                                                     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! { 
 | ||
|                                                             <div class={class_name} 
 | ||
|                                                                  title={title.clone()} 
 | ||
|                                                                  onclick={event_click}
 | ||
|                                                                  oncontextmenu={event_context_menu}
 | ||
|                                                                  style={format!("background-color: {}", event_color)}>
 | ||
|                                                                 {
 | ||
|                                                                     if is_refreshing {
 | ||
|                                                                         "🔄 Refreshing...".to_string()
 | ||
|                                                                     } else if title.len() > 15 {
 | ||
|                                                                         format!("{}...", &title[..12])
 | ||
|                                                                     } else {
 | ||
|                                                                         title
 | ||
|                                                                     }
 | ||
|                                                                 }
 | ||
|                                                             </div> 
 | ||
|                                                         }
 | ||
|                                                     }).collect::<Html>()
 | ||
|                                                 }
 | ||
|                                                 {
 | ||
|                                                     if events.len() > 2 {
 | ||
|                                                         html! { <div class="more-events">{format!("+{} more", events.len() - 2)}</div> }
 | ||
|                                                     } else {
 | ||
|                                                         html! {}
 | ||
|                                                     }
 | ||
|                                                 }
 | ||
|                                             </div>
 | ||
|                                         }
 | ||
|                                     } else {
 | ||
|                                         html! {}
 | ||
|                                     }
 | ||
|                                 }
 | ||
|                             </div>
 | ||
|                         }
 | ||
|                     }).collect::<Html>()
 | ||
|                 }
 | ||
|                 
 | ||
|                 { render_next_month_days(days_from_prev_month.len(), days_in_month) }
 | ||
|             </div>
 | ||
|             
 | ||
|             // Event details modal
 | ||
|             <EventModal 
 | ||
|                 event={(*selected_event).clone()}
 | ||
|                 on_close={{
 | ||
|                     let selected_event_clone = selected_event.clone();
 | ||
|                     Callback::from(move |_| {
 | ||
|                         selected_event_clone.set(None);
 | ||
|                     })
 | ||
|                 }}
 | ||
|             />
 | ||
|         </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 {
 | ||
|         // 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"
 | ||
|     }
 | ||
| }
 | ||
| 
 |