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 show_create_modal = use_state(|| false);
|
||||||
let create_event_data = use_state(|| None::<(chrono::NaiveDate, chrono::NaiveTime, chrono::NaiveTime)>);
|
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
|
// Handle view mode changes - adjust current_date format when switching between month/week
|
||||||
{
|
{
|
||||||
let current_date = current_date.clone();
|
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
|
// Handle drag-to-create event
|
||||||
let on_create_event = {
|
let on_create_event = {
|
||||||
let show_create_modal = show_create_modal.clone();
|
let show_create_modal = show_create_modal.clone();
|
||||||
@@ -172,6 +197,8 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
|||||||
on_prev={on_prev}
|
on_prev={on_prev}
|
||||||
on_next={on_next}
|
on_next={on_next}
|
||||||
on_today={on_today}
|
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_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
||||||
on_create_event={Some(on_create_event)}
|
on_create_event={Some(on_create_event)}
|
||||||
context_menus_open={props.context_menus_open}
|
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_prev: Callback<MouseEvent>,
|
||||||
pub on_next: Callback<MouseEvent>,
|
pub on_next: Callback<MouseEvent>,
|
||||||
pub on_today: 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)]
|
#[function_component(CalendarHeader)]
|
||||||
@@ -18,7 +22,20 @@ pub fn calendar_header(props: &CalendarHeaderProps) -> Html {
|
|||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="calendar-header">
|
<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>
|
<h2 class="month-year">{title}</h2>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button class="today-button" onclick={props.on_today.clone()}>{"Today"}</button>
|
<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)>>,
|
pub on_create_event: Option<Callback<(NaiveDate, NaiveDateTime, NaiveDateTime)>>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub context_menus_open: bool,
|
pub context_menus_open: bool,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub time_increment: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
@@ -122,6 +124,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
let onmousedown = {
|
let onmousedown = {
|
||||||
let drag_state = drag_state_clone.clone();
|
let drag_state = drag_state_clone.clone();
|
||||||
let context_menus_open = props.context_menus_open;
|
let context_menus_open = props.context_menus_open;
|
||||||
|
let time_increment = props.time_increment;
|
||||||
Callback::from(move |e: MouseEvent| {
|
Callback::from(move |e: MouseEvent| {
|
||||||
// Don't start drag if any context menu is open
|
// Don't start drag if any context menu is open
|
||||||
if context_menus_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 = e.layer_y() as f64;
|
||||||
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||||
|
|
||||||
// Snap to 15-minute increments
|
// Snap to increment
|
||||||
let snapped_y = snap_to_15_minutes(relative_y);
|
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||||
|
|
||||||
drag_state.set(Some(DragState {
|
drag_state.set(Some(DragState {
|
||||||
is_dragging: true,
|
is_dragging: true,
|
||||||
@@ -153,6 +156,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
|
|
||||||
let onmousemove = {
|
let onmousemove = {
|
||||||
let drag_state = drag_state_clone.clone();
|
let drag_state = drag_state_clone.clone();
|
||||||
|
let time_increment = props.time_increment;
|
||||||
Callback::from(move |e: MouseEvent| {
|
Callback::from(move |e: MouseEvent| {
|
||||||
if let Some(mut current_drag) = (*drag_state).clone() {
|
if let Some(mut current_drag) = (*drag_state).clone() {
|
||||||
if current_drag.is_dragging {
|
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 = e.layer_y() as f64;
|
||||||
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||||
|
|
||||||
// Snap to 15-minute increments
|
// Snap to increment
|
||||||
let snapped_y = snap_to_15_minutes(relative_y);
|
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||||
|
|
||||||
current_drag.current_y = snapped_y;
|
current_drag.current_y = snapped_y;
|
||||||
drag_state.set(Some(current_drag));
|
drag_state.set(Some(current_drag));
|
||||||
@@ -214,7 +218,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
{onmousemove}
|
{onmousemove}
|
||||||
{onmouseup}
|
{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| {
|
(0..24).map(|_hour| {
|
||||||
html! {
|
html! {
|
||||||
@@ -225,8 +229,11 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
}
|
}
|
||||||
}).collect::<Html>()
|
}).collect::<Html>()
|
||||||
}
|
}
|
||||||
// Final boundary slot to match the final time label
|
// Final boundary slot to complete the 24-hour visual grid - make it interactive like other slots
|
||||||
<div class="time-slot boundary-slot"></div>
|
<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
|
// Events positioned absolutely based on their actual times
|
||||||
<div class="events-container">
|
<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
|
// Calculate the pixel position of an event based on its time
|
||||||
// Each hour is 60px, so we convert time to pixels
|
// Each hour is 60px, so we convert time to pixels
|
||||||
// Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes)
|
// Snap pixel position to 15-minute increments (15px = 15 minutes since 60px = 60 minutes)
|
||||||
fn snap_to_15_minutes(pixels: f64) -> f64 {
|
fn snap_to_increment(pixels: f64, increment: u32) -> f64 {
|
||||||
let increment = 15.0; // 15px = 15 minutes
|
let increment_px = increment as f64; // Convert to pixels (1px = 1 minute)
|
||||||
(pixels / increment).round() * increment
|
(pixels / increment_px).round() * increment_px
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert pixel position to time (inverse of time to pixels)
|
// 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 hours = (total_minutes / 60.0) as u32;
|
||||||
let minutes = (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 hours = hours.min(23);
|
||||||
let minutes = minutes.min(59);
|
let minutes = minutes.min(59);
|
||||||
|
|
||||||
|
|||||||
26
styles.css
26
styles.css
@@ -410,12 +410,38 @@ body {
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
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 {
|
.nav-button {
|
||||||
background: rgba(255,255,255,0.2);
|
background: rgba(255,255,255,0.2);
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user