Add configurable time increment toggle for event creation
- Add toggle button (15/30 minutes) in calendar header next to navigation arrows - Implement circular frosted styling consistent with nav buttons - Add configurable snapping for drag-to-create events in week view - Persist time increment setting across browser sessions using localStorage - Update snap function to accept configurable increment instead of hardcoded 15 minutes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,20 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
let show_create_modal = use_state(|| false);
|
||||
let create_event_data = use_state(|| None::<(chrono::NaiveDate, chrono::NaiveTime, chrono::NaiveTime)>);
|
||||
|
||||
// State for time increment snapping (15 or 30 minutes)
|
||||
let time_increment = use_state(|| {
|
||||
// Try to load saved time increment from localStorage
|
||||
if let Ok(saved_increment) = LocalStorage::get::<u32>("calendar_time_increment") {
|
||||
if saved_increment == 15 || saved_increment == 30 {
|
||||
saved_increment
|
||||
} else {
|
||||
15
|
||||
}
|
||||
} else {
|
||||
15
|
||||
}
|
||||
});
|
||||
|
||||
// Handle view mode changes - adjust current_date format when switching between month/week
|
||||
{
|
||||
let current_date = current_date.clone();
|
||||
@@ -152,6 +166,17 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
// Handle time increment toggle
|
||||
let on_time_increment_toggle = {
|
||||
let time_increment = time_increment.clone();
|
||||
Callback::from(move |_: MouseEvent| {
|
||||
let current = *time_increment;
|
||||
let next = if current == 15 { 30 } else { 15 };
|
||||
time_increment.set(next);
|
||||
let _ = LocalStorage::set("calendar_time_increment", next);
|
||||
})
|
||||
};
|
||||
|
||||
// Handle drag-to-create event
|
||||
let on_create_event = {
|
||||
let show_create_modal = show_create_modal.clone();
|
||||
@@ -172,6 +197,8 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
on_prev={on_prev}
|
||||
on_next={on_next}
|
||||
on_today={on_today}
|
||||
time_increment={Some(*time_increment)}
|
||||
on_time_increment_toggle={Some(on_time_increment_toggle)}
|
||||
/>
|
||||
|
||||
{
|
||||
@@ -212,6 +239,7 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
||||
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
||||
on_create_event={Some(on_create_event)}
|
||||
context_menus_open={props.context_menus_open}
|
||||
time_increment={*time_increment}
|
||||
/>
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ pub struct CalendarHeaderProps {
|
||||
pub on_prev: Callback<MouseEvent>,
|
||||
pub on_next: Callback<MouseEvent>,
|
||||
pub on_today: Callback<MouseEvent>,
|
||||
#[prop_or_default]
|
||||
pub time_increment: Option<u32>,
|
||||
#[prop_or_default]
|
||||
pub on_time_increment_toggle: Option<Callback<MouseEvent>>,
|
||||
}
|
||||
|
||||
#[function_component(CalendarHeader)]
|
||||
@@ -18,7 +22,20 @@ pub fn calendar_header(props: &CalendarHeaderProps) -> Html {
|
||||
|
||||
html! {
|
||||
<div class="calendar-header">
|
||||
<button class="nav-button" onclick={props.on_prev.clone()}>{"‹"}</button>
|
||||
<div class="header-left">
|
||||
<button class="nav-button" onclick={props.on_prev.clone()}>{"‹"}</button>
|
||||
{
|
||||
if let (Some(increment), Some(callback)) = (props.time_increment, &props.on_time_increment_toggle) {
|
||||
html! {
|
||||
<button class="time-increment-button" onclick={callback.clone()}>
|
||||
{format!("{}", increment)}
|
||||
</button>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<h2 class="month-year">{title}</h2>
|
||||
<div class="header-right">
|
||||
<button class="today-button" onclick={props.on_today.clone()}>{"Today"}</button>
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct WeekViewProps {
|
||||
pub on_create_event: Option<Callback<(NaiveDate, NaiveDateTime, NaiveDateTime)>>,
|
||||
#[prop_or_default]
|
||||
pub context_menus_open: bool,
|
||||
#[prop_or_default]
|
||||
pub time_increment: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@@ -122,6 +124,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let onmousedown = {
|
||||
let drag_state = drag_state_clone.clone();
|
||||
let context_menus_open = props.context_menus_open;
|
||||
let time_increment = props.time_increment;
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
// Don't start drag if any context menu is open
|
||||
if context_menus_open {
|
||||
@@ -138,8 +141,8 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let relative_y = e.layer_y() as f64;
|
||||
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||
|
||||
// Snap to 15-minute increments
|
||||
let snapped_y = snap_to_15_minutes(relative_y);
|
||||
// Snap to increment
|
||||
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||
|
||||
drag_state.set(Some(DragState {
|
||||
is_dragging: true,
|
||||
@@ -153,6 +156,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
|
||||
let onmousemove = {
|
||||
let drag_state = drag_state_clone.clone();
|
||||
let time_increment = props.time_increment;
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
if let Some(mut current_drag) = (*drag_state).clone() {
|
||||
if current_drag.is_dragging {
|
||||
@@ -160,8 +164,8 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let relative_y = e.layer_y() as f64;
|
||||
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||
|
||||
// Snap to 15-minute increments
|
||||
let snapped_y = snap_to_15_minutes(relative_y);
|
||||
// Snap to increment
|
||||
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||
|
||||
current_drag.current_y = snapped_y;
|
||||
drag_state.set(Some(current_drag));
|
||||
@@ -214,7 +218,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
{onmousemove}
|
||||
{onmouseup}
|
||||
>
|
||||
// Time slot backgrounds - 24 full hour slots + 1 boundary slot
|
||||
// Time slot backgrounds - 24 hour slots to represent full day
|
||||
{
|
||||
(0..24).map(|_hour| {
|
||||
html! {
|
||||
@@ -225,8 +229,11 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
// Final boundary slot to match the final time label
|
||||
<div class="time-slot boundary-slot"></div>
|
||||
// Final boundary slot to complete the 24-hour visual grid - make it interactive like other slots
|
||||
<div class="time-slot boundary-slot">
|
||||
<div class="time-slot-half"></div>
|
||||
<div class="time-slot-half"></div>
|
||||
</div>
|
||||
|
||||
// Events positioned absolutely based on their actual times
|
||||
<div class="events-container">
|
||||
@@ -398,9 +405,9 @@ fn get_weekday_name(weekday: Weekday) -> &'static str {
|
||||
// Calculate the pixel position of an event based on its time
|
||||
// Each hour is 60px, so we convert time to pixels
|
||||
// Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes)
|
||||
fn snap_to_15_minutes(pixels: f64) -> f64 {
|
||||
let increment = 15.0; // 15px = 15 minutes
|
||||
(pixels / increment).round() * increment
|
||||
fn snap_to_increment(pixels: f64, increment: u32) -> f64 {
|
||||
let increment_px = increment as f64; // Convert to pixels (1px = 1 minute)
|
||||
(pixels / increment_px).round() * increment_px
|
||||
}
|
||||
|
||||
// Convert pixel position to time (inverse of time to pixels)
|
||||
@@ -410,7 +417,12 @@ fn pixels_to_time(pixels: f64) -> NaiveTime {
|
||||
let hours = (total_minutes / 60.0) as u32;
|
||||
let minutes = (total_minutes % 60.0) as u32;
|
||||
|
||||
// Clamp to valid time range
|
||||
// Handle midnight boundary - if we're at exactly 1440 pixels (24:00), return midnight
|
||||
if total_minutes >= 1440.0 {
|
||||
return NaiveTime::from_hms_opt(0, 0, 0).unwrap();
|
||||
}
|
||||
|
||||
// Clamp to valid time range for within-day times
|
||||
let hours = hours.min(23);
|
||||
let minutes = minutes.min(59);
|
||||
|
||||
|
||||
26
styles.css
26
styles.css
@@ -410,12 +410,38 @@ body {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.time-increment-button {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.time-increment-button:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
|
||||
Reference in New Issue
Block a user