Implement comprehensive calendar UX improvements
- 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 <noreply@anthropic.com>
This commit is contained in:
22
src/app.rs
22
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<NaiveDate> { 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::<String>("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);
|
||||
})
|
||||
};
|
||||
|
||||
@@ -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::<String>("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::<String>("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::<String>("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::<CalendarEvent>);
|
||||
|
||||
// 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! {
|
||||
<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::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! {
|
||||
<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()}
|
||||
selected_date={Some(*selected_date)}
|
||||
on_day_select={Some(on_day_select)}
|
||||
/>
|
||||
}
|
||||
},
|
||||
ViewMode::Week => html! {
|
||||
<WeekView
|
||||
|
||||
@@ -14,14 +14,7 @@ pub struct CalendarHeaderProps {
|
||||
|
||||
#[function_component(CalendarHeader)]
|
||||
pub fn calendar_header(props: &CalendarHeaderProps) -> 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! {
|
||||
<div class="calendar-header">
|
||||
|
||||
@@ -18,6 +18,10 @@ pub struct MonthViewProps {
|
||||
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 selected_date: Option<NaiveDate>,
|
||||
#[prop_or_default]
|
||||
pub on_day_select: Option<Callback<NaiveDate>>,
|
||||
}
|
||||
|
||||
#[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! {
|
||||
<div
|
||||
class={classes!("calendar-day", if is_today { Some("today") } else { None })}
|
||||
class={classes!(
|
||||
"calendar-day",
|
||||
if is_today { Some("today") } else { None },
|
||||
if is_selected { Some("selected") } else { None }
|
||||
)}
|
||||
onclick={
|
||||
if let Some(callback) = &props.on_day_select {
|
||||
let callback = callback.clone();
|
||||
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
||||
e.stop_propagation(); // Prevent other handlers
|
||||
callback.emit(date);
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
oncontextmenu={
|
||||
if let Some(callback) = &props.on_calendar_context_menu {
|
||||
let callback = callback.clone();
|
||||
|
||||
@@ -147,7 +147,8 @@ pub fn week_view(props: &WeekViewProps) -> 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! {
|
||||
|
||||
Reference in New Issue
Block a user