From a8bb2c8164c5a935a91d607d2c97c7e5027ae9e1 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Fri, 29 Aug 2025 10:44:44 -0400 Subject: [PATCH] Implement comprehensive calendar UX improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/app.rs | 22 ++++- src/components/calendar.rs | 133 ++++++++++++++++++++++-------- src/components/calendar_header.rs | 9 +- src/components/month_view.rs | 22 ++++- src/components/week_view.rs | 29 ++++++- 5 files changed, 169 insertions(+), 46 deletions(-) diff --git a/src/app.rs b/src/app.rs index e70af75..574c909 100644 --- a/src/app.rs +++ b/src/app.rs @@ -28,8 +28,18 @@ pub fn App() -> Html { let create_event_modal_open = use_state(|| false); let selected_date_for_event = use_state(|| -> Option { None }); - // Calendar view state - let current_view = use_state(|| ViewMode::Month); + // Calendar view state - load from localStorage if available + let current_view = use_state(|| { + // Try to load saved view mode from localStorage + if let Ok(saved_view) = LocalStorage::get::("calendar_view_mode") { + match saved_view.as_str() { + "week" => ViewMode::Week, + _ => ViewMode::Month, + } + } else { + ViewMode::Month // Default to month view + } + }); let available_colors = [ "#3B82F6", "#10B981", "#F59E0B", "#EF4444", @@ -58,6 +68,14 @@ pub fn App() -> Html { let on_view_change = { let current_view = current_view.clone(); Callback::from(move |new_view: ViewMode| { + // Save view mode to localStorage + let view_string = match new_view { + ViewMode::Month => "month", + ViewMode::Week => "week", + }; + let _ = LocalStorage::set("calendar_view_mode", view_string); + + // Update state current_view.set(new_view); }) }; diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 8f5ee01..5b3a0b6 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -26,66 +26,121 @@ pub struct CalendarProps { #[function_component] pub fn Calendar(props: &CalendarProps) -> Html { let today = Local::now().date_naive(); - let current_date = use_state(|| { - // Try to load saved date from localStorage - if let Ok(saved_date_str) = LocalStorage::get::("calendar_current_month") { + // Track the currently selected date (the actual day the user has selected) + let selected_date = use_state(|| { + // Try to load saved selected date from localStorage + if let Ok(saved_date_str) = LocalStorage::get::("calendar_selected_date") { if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { - saved_date.with_day(1).unwrap_or(today) + saved_date } else { today } } else { - today + // Check for old key for backward compatibility + if let Ok(saved_date_str) = LocalStorage::get::("calendar_current_month") { + if let Ok(saved_date) = NaiveDate::parse_from_str(&saved_date_str, "%Y-%m-%d") { + saved_date + } else { + today + } + } else { + today + } + } + }); + + // Track the display date (what to show in the view) + let current_date = use_state(|| { + match props.view { + ViewMode::Month => selected_date.with_day(1).unwrap_or(*selected_date), + ViewMode::Week => *selected_date, } }); let selected_event = use_state(|| None::); + // Handle view mode changes - adjust current_date format when switching between month/week + { + let current_date = current_date.clone(); + let selected_date = selected_date.clone(); + let view = props.view.clone(); + use_effect_with(view, move |view_mode| { + let selected = *selected_date; + let new_display_date = match view_mode { + ViewMode::Month => selected.with_day(1).unwrap_or(selected), + ViewMode::Week => selected, // Show the week containing the selected date + }; + current_date.set(new_display_date); + || {} + }); + } + let on_prev = { let current_date = current_date.clone(); + let selected_date = selected_date.clone(); let view = props.view.clone(); Callback::from(move |_: MouseEvent| { - let new_date = match view { + let (new_selected, new_display) = match view { ViewMode::Month => { - let prev = *current_date - Duration::days(1); - prev.with_day(1).unwrap() + // Go to previous month, select the 1st day + let prev_month = *current_date - Duration::days(1); + let first_of_prev = prev_month.with_day(1).unwrap(); + (first_of_prev, first_of_prev) + }, + ViewMode::Week => { + // Go to previous week + let prev_week = *selected_date - Duration::weeks(1); + (prev_week, prev_week) }, - 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()); + selected_date.set(new_selected); + current_date.set(new_display); + let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); }) }; let on_next = { let current_date = current_date.clone(); + let selected_date = selected_date.clone(); let view = props.view.clone(); Callback::from(move |_: MouseEvent| { - let new_date = match view { + let (new_selected, new_display) = match view { ViewMode::Month => { - if current_date.month() == 12 { + // Go to next month, select the 1st day + let next_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() - } + }; + (next_month, next_month) + }, + ViewMode::Week => { + // Go to next week + let next_week = *selected_date + Duration::weeks(1); + (next_week, next_week) }, - 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()); + selected_date.set(new_selected); + current_date.set(new_display); + let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); }) }; let on_today = { let current_date = current_date.clone(); + let selected_date = selected_date.clone(); let view = props.view.clone(); Callback::from(move |_| { let today = Local::now().date_naive(); - let new_date = match view { - ViewMode::Month => today.with_day(1).unwrap(), - ViewMode::Week => today, + let (new_selected, new_display) = match view { + ViewMode::Month => { + let first_of_today = today.with_day(1).unwrap(); + (today, first_of_today) // Select today, but display the month + }, + ViewMode::Week => (today, today), // Select and display today }; - current_date.set(new_date); - let _ = LocalStorage::set("calendar_current_month", new_date.format("%Y-%m-%d").to_string()); + selected_date.set(new_selected); + current_date.set(new_display); + let _ = LocalStorage::set("calendar_selected_date", new_selected.format("%Y-%m-%d").to_string()); }) }; @@ -101,17 +156,29 @@ pub fn Calendar(props: &CalendarProps) -> Html { { match props.view { - ViewMode::Month => html! { - + ViewMode::Month => { + let on_day_select = { + let selected_date = selected_date.clone(); + Callback::from(move |date: NaiveDate| { + selected_date.set(date); + let _ = LocalStorage::set("calendar_selected_date", date.format("%Y-%m-%d").to_string()); + }) + }; + + html! { + + } }, ViewMode::Week => html! { Html { - let title = match props.view_mode { - ViewMode::Month => { - format!("{} {}", get_month_name(props.current_date.month()), props.current_date.year()) - }, - ViewMode::Week => { - format!("Week of {} {}", get_month_name(props.current_date.month()), props.current_date.day()) - }, - }; + let title = format!("{} {}", get_month_name(props.current_date.month()), props.current_date.year()); html! {
diff --git a/src/components/month_view.rs b/src/components/month_view.rs index 755a53a..e3adbd8 100644 --- a/src/components/month_view.rs +++ b/src/components/month_view.rs @@ -18,6 +18,10 @@ pub struct MonthViewProps { 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)] @@ -65,11 +69,27 @@ pub fn month_view(props: &MonthViewProps) -> Html { (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! {
Html { let onclick = { let on_event_click = props.on_event_click.clone(); let event = event.clone(); - Callback::from(move |_: MouseEvent| { + Callback::from(move |e: MouseEvent| { + e.stop_propagation(); // Prevent calendar click events from also triggering on_event_click.emit(event.clone()); }) }; @@ -158,6 +159,7 @@ pub fn week_view(props: &WeekViewProps) -> Html { let event = event.clone(); Some(Callback::from(move |e: web_sys::MouseEvent| { e.prevent_default(); + e.stop_propagation(); // Prevent calendar context menu from also triggering callback.emit((e, event.clone())); })) } else { @@ -170,7 +172,30 @@ pub fn week_view(props: &WeekViewProps) -> Html { "All Day".to_string() } else { let local_start = event.start.with_timezone(&Local); - format!("{}", local_start.format("%I:%M %p")) + if let Some(end) = event.end { + let local_end = end.with_timezone(&Local); + + // Check if both times are in same AM/PM period to avoid redundancy + let start_is_am = local_start.hour() < 12; + let end_is_am = local_end.hour() < 12; + + if start_is_am == end_is_am { + // Same AM/PM period - show "9:00 - 10:30 AM" + format!("{} - {}", + local_start.format("%I:%M").to_string().trim_start_matches('0'), + local_end.format("%I:%M %p") + ) + } else { + // Different AM/PM periods - show "9:00 AM - 2:30 PM" + format!("{} - {}", + local_start.format("%I:%M %p"), + local_end.format("%I:%M %p") + ) + } + } else { + // No end time, just show start time + format!("{}", local_start.format("%I:%M %p")) + } }; Some(html! {