Refactor calendar component into modular architecture with view switching
- Split monolithic Calendar component into focused sub-components: - CalendarHeader: Navigation buttons and title display - MonthView: Monthly calendar grid layout and event rendering - WeekView: Weekly calendar view with full-height day containers - Add ViewMode enum for Month/Week view switching in sidebar dropdown - Fix event styling by correcting CSS class from "event" to "event-box" - Implement proper week view layout with full-height day containers - Maintain all existing functionality: event handling, context menus, localStorage persistence 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		| @@ -1,9 +1,9 @@ | ||||
| use yew::prelude::*; | ||||
| use chrono::{Datelike, Local, NaiveDate, Duration, Weekday}; | ||||
| use chrono::{Datelike, Local, NaiveDate, Duration}; | ||||
| use std::collections::HashMap; | ||||
| use web_sys::MouseEvent; | ||||
| use crate::services::calendar_service::{CalendarEvent, UserInfo}; | ||||
| use crate::components::EventModal; | ||||
| use wasm_bindgen::JsCast; | ||||
| use crate::components::{EventModal, ViewMode, CalendarHeader, MonthView, WeekView}; | ||||
| use gloo_storage::{LocalStorage, Storage}; | ||||
|  | ||||
| #[derive(Properties, PartialEq)] | ||||
| @@ -19,17 +19,18 @@ pub struct CalendarProps { | ||||
|     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 view: ViewMode, | ||||
| } | ||||
|  | ||||
| #[function_component] | ||||
| pub fn Calendar(props: &CalendarProps) -> Html { | ||||
|     let today = Local::now().date_naive(); | ||||
|     let current_month = use_state(|| { | ||||
|         // Try to load saved month from localStorage | ||||
|         if let Ok(saved_month_str) = LocalStorage::get::<String>("calendar_current_month") { | ||||
|             if let Ok(saved_month) = NaiveDate::parse_from_str(&saved_month_str, "%Y-%m-%d") { | ||||
|                 // Return the first day of the saved month | ||||
|                 saved_month.with_day(1).unwrap_or(today) | ||||
|     let current_date = use_state(|| { | ||||
|         // Try to load saved date from localStorage | ||||
|         if let Ok(saved_date_str) = LocalStorage::get::<String>("calendar_current_month") { | ||||
|             if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { | ||||
|                 saved_date.with_day(1).unwrap_or(today) | ||||
|             } else { | ||||
|                 today | ||||
|             } | ||||
| @@ -37,217 +38,95 @@ pub fn Calendar(props: &CalendarProps) -> Html { | ||||
|             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); | ||||
|             // Save to localStorage | ||||
|             let _ = LocalStorage::set("calendar_current_month", first_of_prev.format("%Y-%m-%d").to_string()); | ||||
|     let on_prev = { | ||||
|         let current_date = current_date.clone(); | ||||
|         let view = props.view.clone(); | ||||
|         Callback::from(move |_: MouseEvent| { | ||||
|             let new_date = match view { | ||||
|                 ViewMode::Month => { | ||||
|                     let prev = *current_date - Duration::days(1); | ||||
|                     prev.with_day(1).unwrap() | ||||
|                 }, | ||||
|                 ViewMode::Week => *current_date - Duration::weeks(1), | ||||
|             }; | ||||
|             current_date.set(new_date); | ||||
|             let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").to_string()); | ||||
|         }) | ||||
|     }; | ||||
|      | ||||
|     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() | ||||
|     let on_next = { | ||||
|         let current_date = current_date.clone(); | ||||
|         let view = props.view.clone(); | ||||
|         Callback::from(move |_: MouseEvent| { | ||||
|             let new_date = match view { | ||||
|                 ViewMode::Month => { | ||||
|                     if current_date.month() == 12 { | ||||
|                         NaiveDate::from_ymd_opt(current_date.year() + 1, 1, 1).unwrap() | ||||
|                     } else { | ||||
|                         NaiveDate::from_ymd_opt(current_date.year(), current_date.month() + 1, 1).unwrap() | ||||
|                     } | ||||
|                 }, | ||||
|                 ViewMode::Week => *current_date + Duration::weeks(1), | ||||
|             }; | ||||
|             current_month.set(next); | ||||
|             // Save to localStorage | ||||
|             let _ = LocalStorage::set("calendar_current_month", next.format("%Y-%m-%d").to_string()); | ||||
|             current_date.set(new_date); | ||||
|             let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").to_string()); | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     let go_to_today = { | ||||
|         let current_month = current_month.clone(); | ||||
|     let on_today = { | ||||
|         let current_date = current_date.clone(); | ||||
|         let view = props.view.clone(); | ||||
|         Callback::from(move |_| { | ||||
|             let today = Local::now().date_naive(); | ||||
|             let first_of_today_month = today.with_day(1).unwrap(); | ||||
|             current_month.set(first_of_today_month); | ||||
|             // Save to localStorage | ||||
|             let _ = LocalStorage::set("calendar_current_month", first_of_today_month.format("%Y-%m-%d").to_string()); | ||||
|             let new_date = match view { | ||||
|                 ViewMode::Month => today.with_day(1).unwrap(), | ||||
|                 ViewMode::Week => today, | ||||
|             }; | ||||
|             current_date.set(new_date); | ||||
|             let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").to_string()); | ||||
|         }) | ||||
|     }; | ||||
|      | ||||
|  | ||||
|     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> | ||||
|                 <div class="header-right"> | ||||
|                     <button class="today-button" onclick={go_to_today}>{"Today"}</button> | ||||
|                     <button class="nav-button" onclick={next_month}>{"›"}</button> | ||||
|                 </div> | ||||
|             </div> | ||||
|         <div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}> | ||||
|             <CalendarHeader  | ||||
|                 current_date={*current_date} | ||||
|                 view_mode={props.view.clone()} | ||||
|                 on_prev={on_prev} | ||||
|                 on_next={on_next} | ||||
|                 on_today={on_today} | ||||
|             /> | ||||
|              | ||||
|             <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>() | ||||
|             { | ||||
|                 match props.view { | ||||
|                     ViewMode::Month => html! { | ||||
|                         <MonthView | ||||
|                             current_month={*current_date} | ||||
|                             today={today} | ||||
|                             events={props.events.clone()} | ||||
|                             on_event_click={props.on_event_click.clone()} | ||||
|                             refreshing_event_uid={props.refreshing_event_uid.clone()} | ||||
|                             user_info={props.user_info.clone()} | ||||
|                             on_event_context_menu={props.on_event_context_menu.clone()} | ||||
|                             on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||
|                         /> | ||||
|                     }, | ||||
|                     ViewMode::Week => html! { | ||||
|                         <WeekView | ||||
|                             current_date={*current_date} | ||||
|                             today={today} | ||||
|                             events={props.events.clone()} | ||||
|                             on_event_click={props.on_event_click.clone()} | ||||
|                             refreshing_event_uid={props.refreshing_event_uid.clone()} | ||||
|                             user_info={props.user_info.clone()} | ||||
|                             on_event_context_menu={props.on_event_context_menu.clone()} | ||||
|                             on_calendar_context_menu={props.on_calendar_context_menu.clone()} | ||||
|                         /> | ||||
|                     }, | ||||
|                 } | ||||
|                  | ||||
|                 // 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  | ||||
| @@ -261,73 +140,4 @@ pub fn Calendar(props: &CalendarProps) -> Html { | ||||
|             /> | ||||
|         </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" | ||||
|     } | ||||
| } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone