Compare commits
4 Commits
edb216347d
...
d36609d8c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36609d8c2 | ||
|
|
e23278d71e | ||
|
|
edd209238f | ||
|
|
4fbef8a5dc |
121
src/app.rs
121
src/app.rs
@@ -138,7 +138,20 @@ pub fn App() -> Html {
|
|||||||
let context_menu_open = context_menu_open.clone();
|
let context_menu_open = context_menu_open.clone();
|
||||||
let event_context_menu_open = event_context_menu_open.clone();
|
let event_context_menu_open = event_context_menu_open.clone();
|
||||||
let calendar_context_menu_open = calendar_context_menu_open.clone();
|
let calendar_context_menu_open = calendar_context_menu_open.clone();
|
||||||
Callback::from(move |_: MouseEvent| {
|
Callback::from(move |e: MouseEvent| {
|
||||||
|
// Check if any context menu or color picker is open
|
||||||
|
let any_menu_open = color_picker_open.is_some() ||
|
||||||
|
*context_menu_open ||
|
||||||
|
*event_context_menu_open ||
|
||||||
|
*calendar_context_menu_open;
|
||||||
|
|
||||||
|
if any_menu_open {
|
||||||
|
// Prevent the default action and stop event propagation
|
||||||
|
e.prevent_default();
|
||||||
|
e.stop_propagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all open menus/pickers
|
||||||
color_picker_open.set(None);
|
color_picker_open.set(None);
|
||||||
context_menu_open.set(false);
|
context_menu_open.set(false);
|
||||||
event_context_menu_open.set(false);
|
event_context_menu_open.set(false);
|
||||||
@@ -146,6 +159,12 @@ pub fn App() -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Compute if any context menu is open
|
||||||
|
let any_context_menu_open = color_picker_open.is_some() ||
|
||||||
|
*context_menu_open ||
|
||||||
|
*event_context_menu_open ||
|
||||||
|
*calendar_context_menu_open;
|
||||||
|
|
||||||
let on_color_change = {
|
let on_color_change = {
|
||||||
let user_info = user_info.clone();
|
let user_info = user_info.clone();
|
||||||
let color_picker_open = color_picker_open.clone();
|
let color_picker_open = color_picker_open.clone();
|
||||||
@@ -326,6 +345,102 @@ pub fn App() -> Html {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_event_update = {
|
||||||
|
let auth_token = auth_token.clone();
|
||||||
|
Callback::from(move |(original_event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| {
|
||||||
|
web_sys::console::log_1(&format!("Updating event: {} to new times: {} - {}",
|
||||||
|
original_event.uid,
|
||||||
|
new_start.format("%Y-%m-%d %H:%M"),
|
||||||
|
new_end.format("%Y-%m-%d %H:%M")).into());
|
||||||
|
|
||||||
|
if let Some(token) = (*auth_token).clone() {
|
||||||
|
let original_event = original_event.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let calendar_service = CalendarService::new();
|
||||||
|
|
||||||
|
// Get CalDAV password from storage
|
||||||
|
let password = if let Ok(credentials_str) = LocalStorage::get::<String>("caldav_credentials") {
|
||||||
|
if let Ok(credentials) = serde_json::from_str::<serde_json::Value>(&credentials_str) {
|
||||||
|
credentials["password"].as_str().unwrap_or("").to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert local times to UTC for backend storage
|
||||||
|
let start_utc = new_start.and_local_timezone(chrono::Local).unwrap().to_utc();
|
||||||
|
let end_utc = new_end.and_local_timezone(chrono::Local).unwrap().to_utc();
|
||||||
|
|
||||||
|
// Format UTC date and time strings for backend
|
||||||
|
let start_date = start_utc.format("%Y-%m-%d").to_string();
|
||||||
|
let start_time = start_utc.format("%H:%M").to_string();
|
||||||
|
let end_date = end_utc.format("%Y-%m-%d").to_string();
|
||||||
|
let end_time = end_utc.format("%H:%M").to_string();
|
||||||
|
|
||||||
|
// Convert existing event data to string formats for the API
|
||||||
|
let status_str = match original_event.status {
|
||||||
|
crate::services::calendar_service::EventStatus::Tentative => "TENTATIVE".to_string(),
|
||||||
|
crate::services::calendar_service::EventStatus::Confirmed => "CONFIRMED".to_string(),
|
||||||
|
crate::services::calendar_service::EventStatus::Cancelled => "CANCELLED".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let class_str = match original_event.class {
|
||||||
|
crate::services::calendar_service::EventClass::Public => "PUBLIC".to_string(),
|
||||||
|
crate::services::calendar_service::EventClass::Private => "PRIVATE".to_string(),
|
||||||
|
crate::services::calendar_service::EventClass::Confidential => "CONFIDENTIAL".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert reminders to string format
|
||||||
|
let reminder_str = if !original_event.reminders.is_empty() {
|
||||||
|
format!("{}", original_event.reminders[0].minutes_before)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle recurrence (keep existing)
|
||||||
|
let recurrence_str = original_event.recurrence_rule.unwrap_or_default();
|
||||||
|
let recurrence_days = vec![false; 7]; // Default - could be enhanced to parse existing recurrence
|
||||||
|
|
||||||
|
match calendar_service.update_event(
|
||||||
|
&token,
|
||||||
|
&password,
|
||||||
|
original_event.uid,
|
||||||
|
original_event.summary.unwrap_or_default(),
|
||||||
|
original_event.description.unwrap_or_default(),
|
||||||
|
start_date,
|
||||||
|
start_time,
|
||||||
|
end_date,
|
||||||
|
end_time,
|
||||||
|
original_event.location.unwrap_or_default(),
|
||||||
|
original_event.all_day,
|
||||||
|
status_str,
|
||||||
|
class_str,
|
||||||
|
original_event.priority,
|
||||||
|
original_event.organizer.unwrap_or_default(),
|
||||||
|
original_event.attendees.join(","),
|
||||||
|
original_event.categories.join(","),
|
||||||
|
reminder_str,
|
||||||
|
recurrence_str,
|
||||||
|
recurrence_days,
|
||||||
|
original_event.calendar_path
|
||||||
|
).await {
|
||||||
|
Ok(_) => {
|
||||||
|
web_sys::console::log_1(&"Event updated successfully".into());
|
||||||
|
// Trigger a page reload to refresh events from all calendars
|
||||||
|
web_sys::window().unwrap().location().reload().unwrap();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
web_sys::console::error_1(&format!("Failed to update event: {}", err).into());
|
||||||
|
web_sys::window().unwrap().alert_with_message(&format!("Failed to update event: {}", err)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
let refresh_calendars = {
|
let refresh_calendars = {
|
||||||
let auth_token = auth_token.clone();
|
let auth_token = auth_token.clone();
|
||||||
let user_info = user_info.clone();
|
let user_info = user_info.clone();
|
||||||
@@ -401,6 +516,8 @@ pub fn App() -> Html {
|
|||||||
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
||||||
view={(*current_view).clone()}
|
view={(*current_view).clone()}
|
||||||
on_create_event_request={Some(on_event_create.clone())}
|
on_create_event_request={Some(on_event_create.clone())}
|
||||||
|
on_event_update_request={Some(on_event_update.clone())}
|
||||||
|
context_menus_open={any_context_menu_open}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
@@ -414,7 +531,9 @@ pub fn App() -> Html {
|
|||||||
on_login={on_login.clone()}
|
on_login={on_login.clone()}
|
||||||
on_event_context_menu={Some(on_event_context_menu.clone())}
|
on_event_context_menu={Some(on_event_context_menu.clone())}
|
||||||
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
||||||
|
on_event_update_request={Some(on_event_update.clone())}
|
||||||
on_create_event_request={Some(on_event_create.clone())}
|
on_create_event_request={Some(on_event_create.clone())}
|
||||||
|
context_menus_open={any_context_menu_open}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ pub struct CalendarProps {
|
|||||||
pub view: ViewMode,
|
pub view: ViewMode,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub on_create_event_request: Option<Callback<EventCreationData>>,
|
pub on_create_event_request: Option<Callback<EventCreationData>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub context_menus_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
@@ -64,6 +68,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();
|
||||||
@@ -150,6 +168,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();
|
||||||
@@ -161,6 +190,16 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
|||||||
show_create_modal.set(true);
|
show_create_modal.set(true);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle drag-to-move event
|
||||||
|
let on_event_update = {
|
||||||
|
let on_event_update_request = props.on_event_update_request.clone();
|
||||||
|
Callback::from(move |(event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| {
|
||||||
|
if let Some(callback) = &on_event_update_request {
|
||||||
|
callback.emit((event, new_start, new_end));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}>
|
<div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}>
|
||||||
@@ -170,6 +209,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)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -209,6 +250,9 @@ pub fn Calendar(props: &CalendarProps) -> Html {
|
|||||||
on_event_context_menu={props.on_event_context_menu.clone()}
|
on_event_context_menu={props.on_event_context_menu.clone()}
|
||||||
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)}
|
||||||
|
on_event_update={Some(on_event_update)}
|
||||||
|
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>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use chrono::{Datelike, NaiveDate, Weekday};
|
use chrono::{Datelike, NaiveDate, Weekday};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use web_sys::MouseEvent;
|
use web_sys::window;
|
||||||
|
use wasm_bindgen::{prelude::*, JsCast};
|
||||||
use crate::services::calendar_service::{CalendarEvent, UserInfo};
|
use crate::services::calendar_service::{CalendarEvent, UserInfo};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
@@ -26,11 +27,58 @@ pub struct MonthViewProps {
|
|||||||
|
|
||||||
#[function_component(MonthView)]
|
#[function_component(MonthView)]
|
||||||
pub fn month_view(props: &MonthViewProps) -> Html {
|
pub fn month_view(props: &MonthViewProps) -> Html {
|
||||||
|
let max_events_per_day = use_state(|| 4); // Default to 4 events max
|
||||||
let first_day_of_month = props.current_month.with_day(1).unwrap();
|
let first_day_of_month = props.current_month.with_day(1).unwrap();
|
||||||
let days_in_month = get_days_in_month(props.current_month);
|
let days_in_month = get_days_in_month(props.current_month);
|
||||||
let first_weekday = first_day_of_month.weekday();
|
let first_weekday = first_day_of_month.weekday();
|
||||||
let days_from_prev_month = get_days_from_previous_month(props.current_month, first_weekday);
|
let days_from_prev_month = get_days_from_previous_month(props.current_month, first_weekday);
|
||||||
|
|
||||||
|
// Calculate maximum events that can fit based on available height
|
||||||
|
let calculate_max_events = {
|
||||||
|
let max_events_per_day = max_events_per_day.clone();
|
||||||
|
move || {
|
||||||
|
// Since we're using CSS Grid with equal row heights,
|
||||||
|
// we can estimate based on typical calendar dimensions
|
||||||
|
// Typical calendar height is around 600-800px for 6 rows
|
||||||
|
// Each row gets ~100-133px, minus day number and padding leaves ~70-100px
|
||||||
|
// Each event is ~18px, so we can fit ~3-4 events + "+n more" indicator
|
||||||
|
max_events_per_day.set(3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup resize handler and initial calculation
|
||||||
|
{
|
||||||
|
let calculate_max_events = calculate_max_events.clone();
|
||||||
|
use_effect_with((), move |_| {
|
||||||
|
let calculate_max_events_clone = calculate_max_events.clone();
|
||||||
|
|
||||||
|
// Initial calculation with a slight delay to ensure DOM is ready
|
||||||
|
if let Some(window) = window() {
|
||||||
|
let timeout_closure = Closure::wrap(Box::new(move || {
|
||||||
|
calculate_max_events_clone();
|
||||||
|
}) as Box<dyn FnMut()>);
|
||||||
|
|
||||||
|
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
timeout_closure.as_ref().unchecked_ref(),
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
timeout_closure.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup resize listener
|
||||||
|
let resize_closure = Closure::wrap(Box::new(move || {
|
||||||
|
calculate_max_events();
|
||||||
|
}) as Box<dyn Fn()>);
|
||||||
|
|
||||||
|
if let Some(window) = window() {
|
||||||
|
let _ = window.add_event_listener_with_callback("resize", resize_closure.as_ref().unchecked_ref());
|
||||||
|
resize_closure.forget(); // Keep the closure alive
|
||||||
|
}
|
||||||
|
|
||||||
|
|| {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to get calendar color for an event
|
// Helper function to get calendar color for an event
|
||||||
let get_event_color = |event: &CalendarEvent| -> String {
|
let get_event_color = |event: &CalendarEvent| -> String {
|
||||||
if let Some(user_info) = &props.user_info {
|
if let Some(user_info) = &props.user_info {
|
||||||
@@ -72,6 +120,11 @@ pub fn month_view(props: &MonthViewProps) -> Html {
|
|||||||
let is_selected = props.selected_date == Some(date);
|
let is_selected = props.selected_date == Some(date);
|
||||||
let day_events = props.events.get(&date).cloned().unwrap_or_default();
|
let day_events = props.events.get(&date).cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
// Calculate visible events and overflow
|
||||||
|
let max_events = *max_events_per_day as usize;
|
||||||
|
let visible_events: Vec<_> = day_events.iter().take(max_events).collect();
|
||||||
|
let hidden_count = day_events.len().saturating_sub(max_events);
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div
|
<div
|
||||||
class={classes!(
|
class={classes!(
|
||||||
@@ -105,14 +158,14 @@ pub fn month_view(props: &MonthViewProps) -> Html {
|
|||||||
<div class="day-number">{day}</div>
|
<div class="day-number">{day}</div>
|
||||||
<div class="day-events">
|
<div class="day-events">
|
||||||
{
|
{
|
||||||
day_events.iter().map(|event| {
|
visible_events.iter().map(|event| {
|
||||||
let event_color = get_event_color(event);
|
let event_color = get_event_color(event);
|
||||||
let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid);
|
let is_refreshing = props.refreshing_event_uid.as_ref() == Some(&event.uid);
|
||||||
|
|
||||||
let onclick = {
|
let onclick = {
|
||||||
let on_event_click = props.on_event_click.clone();
|
let on_event_click = props.on_event_click.clone();
|
||||||
let event = event.clone();
|
let event = (*event).clone();
|
||||||
Callback::from(move |_: MouseEvent| {
|
Callback::from(move |_: web_sys::MouseEvent| {
|
||||||
on_event_click.emit(event.clone());
|
on_event_click.emit(event.clone());
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -120,7 +173,7 @@ pub fn month_view(props: &MonthViewProps) -> Html {
|
|||||||
let oncontextmenu = {
|
let oncontextmenu = {
|
||||||
if let Some(callback) = &props.on_event_context_menu {
|
if let Some(callback) = &props.on_event_context_menu {
|
||||||
let callback = callback.clone();
|
let callback = callback.clone();
|
||||||
let event = event.clone();
|
let event = (*event).clone();
|
||||||
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
callback.emit((e, event.clone()));
|
callback.emit((e, event.clone()));
|
||||||
@@ -142,6 +195,17 @@ pub fn month_view(props: &MonthViewProps) -> Html {
|
|||||||
}
|
}
|
||||||
}).collect::<Html>()
|
}).collect::<Html>()
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
if hidden_count > 0 {
|
||||||
|
html! {
|
||||||
|
<div class="more-events-indicator">
|
||||||
|
{format!("+{} more", hidden_count)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ pub struct RouteHandlerProps {
|
|||||||
pub view: ViewMode,
|
pub view: ViewMode,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>,
|
pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub context_menus_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(RouteHandler)]
|
#[function_component(RouteHandler)]
|
||||||
@@ -37,6 +41,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
let on_calendar_context_menu = props.on_calendar_context_menu.clone();
|
let on_calendar_context_menu = props.on_calendar_context_menu.clone();
|
||||||
let view = props.view.clone();
|
let view = props.view.clone();
|
||||||
let on_create_event_request = props.on_create_event_request.clone();
|
let on_create_event_request = props.on_create_event_request.clone();
|
||||||
|
let on_event_update_request = props.on_event_update_request.clone();
|
||||||
|
let context_menus_open = props.context_menus_open;
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<Switch<Route> render={move |route| {
|
<Switch<Route> render={move |route| {
|
||||||
@@ -47,6 +53,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
let on_calendar_context_menu = on_calendar_context_menu.clone();
|
let on_calendar_context_menu = on_calendar_context_menu.clone();
|
||||||
let view = view.clone();
|
let view = view.clone();
|
||||||
let on_create_event_request = on_create_event_request.clone();
|
let on_create_event_request = on_create_event_request.clone();
|
||||||
|
let on_event_update_request = on_event_update_request.clone();
|
||||||
|
let context_menus_open = context_menus_open;
|
||||||
|
|
||||||
match route {
|
match route {
|
||||||
Route::Home => {
|
Route::Home => {
|
||||||
@@ -72,6 +80,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|||||||
on_calendar_context_menu={on_calendar_context_menu}
|
on_calendar_context_menu={on_calendar_context_menu}
|
||||||
view={view}
|
view={view}
|
||||||
on_create_event_request={on_create_event_request}
|
on_create_event_request={on_create_event_request}
|
||||||
|
on_event_update_request={on_event_update_request}
|
||||||
|
context_menus_open={context_menus_open}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -94,6 +104,10 @@ pub struct CalendarViewProps {
|
|||||||
pub view: ViewMode,
|
pub view: ViewMode,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>,
|
pub on_create_event_request: Option<Callback<crate::components::EventCreationData>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub context_menus_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
@@ -254,6 +268,8 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html {
|
|||||||
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
||||||
view={props.view.clone()}
|
view={props.view.clone()}
|
||||||
on_create_event_request={props.on_create_event_request.clone()}
|
on_create_event_request={props.on_create_event_request.clone()}
|
||||||
|
on_event_update_request={props.on_event_update_request.clone()}
|
||||||
|
context_menus_open={props.context_menus_open}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -268,6 +284,8 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html {
|
|||||||
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
on_calendar_context_menu={props.on_calendar_context_menu.clone()}
|
||||||
view={props.view.clone()}
|
view={props.view.clone()}
|
||||||
on_create_event_request={props.on_create_event_request.clone()}
|
on_create_event_request={props.on_create_event_request.clone()}
|
||||||
|
on_event_update_request={props.on_event_update_request.clone()}
|
||||||
|
context_menus_open={props.context_menus_open}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,28 @@ pub struct WeekViewProps {
|
|||||||
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub on_create_event: Option<Callback<(NaiveDate, NaiveDateTime, NaiveDateTime)>>,
|
pub on_create_event: Option<Callback<(NaiveDate, NaiveDateTime, NaiveDateTime)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub on_event_update: Option<Callback<(CalendarEvent, NaiveDateTime, NaiveDateTime)>>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub context_menus_open: bool,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub time_increment: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
enum DragType {
|
||||||
|
CreateEvent,
|
||||||
|
MoveEvent(CalendarEvent),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
struct DragState {
|
struct DragState {
|
||||||
is_dragging: bool,
|
is_dragging: bool,
|
||||||
|
drag_type: DragType,
|
||||||
start_date: NaiveDate,
|
start_date: NaiveDate,
|
||||||
start_y: f64,
|
start_y: f64,
|
||||||
current_y: f64,
|
current_y: f64,
|
||||||
|
offset_y: f64, // For event moves, this is the offset from the event's top
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(WeekView)]
|
#[function_component(WeekView)]
|
||||||
@@ -119,7 +133,14 @@ 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 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
|
||||||
|
if context_menus_open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Only handle left-click (button 0)
|
// Only handle left-click (button 0)
|
||||||
if e.button() != 0 {
|
if e.button() != 0 {
|
||||||
return;
|
return;
|
||||||
@@ -130,14 +151,16 @@ 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,
|
||||||
|
drag_type: DragType::CreateEvent,
|
||||||
start_date: date_for_drag,
|
start_date: date_for_drag,
|
||||||
start_y: snapped_y,
|
start_y: snapped_y,
|
||||||
current_y: snapped_y,
|
current_y: snapped_y,
|
||||||
|
offset_y: 0.0,
|
||||||
}));
|
}));
|
||||||
e.prevent_default();
|
e.prevent_default();
|
||||||
})
|
})
|
||||||
@@ -145,6 +168,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 {
|
||||||
@@ -152,8 +176,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));
|
||||||
@@ -165,32 +189,55 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
let onmouseup = {
|
let onmouseup = {
|
||||||
let drag_state = drag_state_clone.clone();
|
let drag_state = drag_state_clone.clone();
|
||||||
let on_create_event = props.on_create_event.clone();
|
let on_create_event = props.on_create_event.clone();
|
||||||
|
let on_event_update = props.on_event_update.clone();
|
||||||
Callback::from(move |_e: MouseEvent| {
|
Callback::from(move |_e: MouseEvent| {
|
||||||
if let Some(current_drag) = (*drag_state).clone() {
|
if let Some(current_drag) = (*drag_state).clone() {
|
||||||
if current_drag.is_dragging {
|
if current_drag.is_dragging {
|
||||||
// Calculate start and end times
|
match ¤t_drag.drag_type {
|
||||||
let start_time = pixels_to_time(current_drag.start_y);
|
DragType::CreateEvent => {
|
||||||
let end_time = pixels_to_time(current_drag.current_y);
|
// Calculate start and end times
|
||||||
|
let start_time = pixels_to_time(current_drag.start_y);
|
||||||
// Ensure start is before end
|
let end_time = pixels_to_time(current_drag.current_y);
|
||||||
let (actual_start, actual_end) = if start_time <= end_time {
|
|
||||||
(start_time, end_time)
|
// Ensure start is before end
|
||||||
} else {
|
let (actual_start, actual_end) = if start_time <= end_time {
|
||||||
(end_time, start_time)
|
(start_time, end_time)
|
||||||
};
|
} else {
|
||||||
|
(end_time, start_time)
|
||||||
// Ensure minimum duration (15 minutes)
|
};
|
||||||
let actual_end = if actual_end.signed_duration_since(actual_start).num_minutes() < 15 {
|
|
||||||
actual_start + chrono::Duration::minutes(15)
|
// Ensure minimum duration (15 minutes)
|
||||||
} else {
|
let actual_end = if actual_end.signed_duration_since(actual_start).num_minutes() < 15 {
|
||||||
actual_end
|
actual_start + chrono::Duration::minutes(15)
|
||||||
};
|
} else {
|
||||||
|
actual_end
|
||||||
let start_datetime = NaiveDateTime::new(current_drag.start_date, actual_start);
|
};
|
||||||
let end_datetime = NaiveDateTime::new(current_drag.start_date, actual_end);
|
|
||||||
|
let start_datetime = NaiveDateTime::new(current_drag.start_date, actual_start);
|
||||||
if let Some(callback) = &on_create_event {
|
let end_datetime = NaiveDateTime::new(current_drag.start_date, actual_end);
|
||||||
callback.emit((current_drag.start_date, start_datetime, end_datetime));
|
|
||||||
|
if let Some(callback) = &on_create_event {
|
||||||
|
callback.emit((current_drag.start_date, start_datetime, end_datetime));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DragType::MoveEvent(event) => {
|
||||||
|
// Calculate new start time based on drag position
|
||||||
|
let new_start_time = pixels_to_time(current_drag.current_y);
|
||||||
|
|
||||||
|
// Calculate duration from original event
|
||||||
|
let original_duration = if let Some(end) = event.end {
|
||||||
|
end.signed_duration_since(event.start)
|
||||||
|
} else {
|
||||||
|
chrono::Duration::hours(1) // Default 1 hour
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time);
|
||||||
|
let new_end_datetime = new_start_datetime + original_duration;
|
||||||
|
|
||||||
|
if let Some(callback) = &on_event_update {
|
||||||
|
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drag_state.set(None);
|
drag_state.set(None);
|
||||||
@@ -206,7 +253,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! {
|
||||||
@@ -217,8 +264,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">
|
||||||
@@ -244,8 +294,41 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let onmousedown_event = {
|
let onmousedown_event = {
|
||||||
|
let drag_state = drag_state.clone();
|
||||||
|
let event_for_drag = event.clone();
|
||||||
|
let date_for_drag = *date;
|
||||||
|
let time_increment = props.time_increment;
|
||||||
Callback::from(move |e: MouseEvent| {
|
Callback::from(move |e: MouseEvent| {
|
||||||
e.stop_propagation(); // Prevent drag-to-create from starting on event clicks
|
e.stop_propagation(); // Prevent drag-to-create from starting on event clicks
|
||||||
|
|
||||||
|
// Only handle left-click (button 0)
|
||||||
|
if e.button() != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Y position relative to the day column
|
||||||
|
let relative_y = e.layer_y() as f64;
|
||||||
|
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||||
|
|
||||||
|
// Get event's current position
|
||||||
|
let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag);
|
||||||
|
let event_start_pixels = event_start_pixels as f64;
|
||||||
|
|
||||||
|
// Calculate offset from the top of the event
|
||||||
|
let offset_y = relative_y - event_start_pixels;
|
||||||
|
|
||||||
|
// Snap to increment
|
||||||
|
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||||
|
|
||||||
|
drag_state.set(Some(DragState {
|
||||||
|
is_dragging: true,
|
||||||
|
drag_type: DragType::MoveEvent(event_for_drag.clone()),
|
||||||
|
start_date: date_for_drag,
|
||||||
|
start_y: snapped_y,
|
||||||
|
current_y: snapped_y,
|
||||||
|
offset_y,
|
||||||
|
}));
|
||||||
|
e.prevent_default();
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -294,31 +377,47 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(html! {
|
// Check if this event is currently being dragged
|
||||||
<div
|
let is_being_dragged = if let Some(drag) = (*drag_state).clone() {
|
||||||
class={classes!(
|
if let DragType::MoveEvent(dragged_event) = &drag.drag_type {
|
||||||
"week-event",
|
dragged_event.uid == event.uid && drag.is_dragging
|
||||||
if is_refreshing { Some("refreshing") } else { None },
|
} else {
|
||||||
if is_all_day { Some("all-day") } else { None }
|
false
|
||||||
)}
|
}
|
||||||
style={format!(
|
} else {
|
||||||
"background-color: {}; top: {}px; height: {}px;",
|
false
|
||||||
event_color,
|
};
|
||||||
start_pixels,
|
|
||||||
duration_pixels
|
if is_being_dragged {
|
||||||
)}
|
// Hide the original event while being dragged
|
||||||
{onclick}
|
Some(html! {})
|
||||||
{oncontextmenu}
|
} else {
|
||||||
onmousedown={onmousedown_event}
|
Some(html! {
|
||||||
>
|
<div
|
||||||
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
|
class={classes!(
|
||||||
{if !is_all_day {
|
"week-event",
|
||||||
html! { <div class="event-time">{time_display}</div> }
|
if is_refreshing { Some("refreshing") } else { None },
|
||||||
} else {
|
if is_all_day { Some("all-day") } else { None }
|
||||||
html! {}
|
)}
|
||||||
}}
|
style={format!(
|
||||||
</div>
|
"background-color: {}; top: {}px; height: {}px;",
|
||||||
})
|
event_color,
|
||||||
|
start_pixels,
|
||||||
|
duration_pixels
|
||||||
|
)}
|
||||||
|
{onclick}
|
||||||
|
{oncontextmenu}
|
||||||
|
onmousedown={onmousedown_event}
|
||||||
|
>
|
||||||
|
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
|
||||||
|
{if !is_all_day {
|
||||||
|
html! { <div class="event-time">{time_display}</div> }
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
}).collect::<Html>()
|
}).collect::<Html>()
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -327,21 +426,48 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
|||||||
{
|
{
|
||||||
if let Some(drag) = (*drag_state).clone() {
|
if let Some(drag) = (*drag_state).clone() {
|
||||||
if drag.is_dragging && drag.start_date == *date {
|
if drag.is_dragging && drag.start_date == *date {
|
||||||
let start_y = drag.start_y.min(drag.current_y);
|
match &drag.drag_type {
|
||||||
let end_y = drag.start_y.max(drag.current_y);
|
DragType::CreateEvent => {
|
||||||
let height = (drag.current_y - drag.start_y).abs().max(20.0);
|
let start_y = drag.start_y.min(drag.current_y);
|
||||||
|
let end_y = drag.start_y.max(drag.current_y);
|
||||||
// Convert pixels to times for display
|
let height = (drag.current_y - drag.start_y).abs().max(20.0);
|
||||||
let start_time = pixels_to_time(start_y);
|
|
||||||
let end_time = pixels_to_time(end_y);
|
// Convert pixels to times for display
|
||||||
|
let start_time = pixels_to_time(start_y);
|
||||||
html! {
|
let end_time = pixels_to_time(end_y);
|
||||||
<div
|
|
||||||
class="temp-event-box"
|
html! {
|
||||||
style={format!("top: {}px; height: {}px;", start_y, height)}
|
<div
|
||||||
>
|
class="temp-event-box"
|
||||||
{format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))}
|
style={format!("top: {}px; height: {}px;", start_y, height)}
|
||||||
</div>
|
>
|
||||||
|
{format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DragType::MoveEvent(event) => {
|
||||||
|
// Show the event being moved at its new position
|
||||||
|
let new_start_time = pixels_to_time(drag.current_y);
|
||||||
|
let original_duration = if let Some(end) = event.end {
|
||||||
|
end.signed_duration_since(event.start)
|
||||||
|
} else {
|
||||||
|
chrono::Duration::hours(1)
|
||||||
|
};
|
||||||
|
let duration_pixels = (original_duration.num_minutes() as f64).max(20.0);
|
||||||
|
let new_end_time = new_start_time + original_duration;
|
||||||
|
|
||||||
|
let event_color = get_event_color(event);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div
|
||||||
|
class="temp-event-box moving-event"
|
||||||
|
style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", drag.current_y, duration_pixels, event_color)}
|
||||||
|
>
|
||||||
|
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
|
||||||
|
<div class="event-time">{format!("{} - {}", new_start_time.format("%I:%M %p"), new_end_time.format("%I:%M %p"))}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
html! {}
|
html! {}
|
||||||
@@ -390,9 +516,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)
|
||||||
@@ -402,7 +528,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);
|
||||||
|
|
||||||
|
|||||||
85
styles.css
85
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;
|
||||||
@@ -458,8 +484,10 @@ body {
|
|||||||
.calendar-grid {
|
.calendar-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
grid-template-rows: auto repeat(6, 1fr);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: white;
|
background: white;
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Week View Container */
|
/* Week View Container */
|
||||||
@@ -668,6 +696,37 @@ body {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Moving event during drag */
|
||||||
|
.temp-event-box.moving-event {
|
||||||
|
background: inherit; /* Use the event's actual color */
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||||
|
color: white;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-event-box.moving-event .event-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-event-box.moving-event .event-time {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.week-event .event-title {
|
.week-event .event-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
@@ -717,12 +776,12 @@ body {
|
|||||||
.calendar-day {
|
.calendar-day {
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
min-height: 100px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-day:hover {
|
.calendar-day:hover {
|
||||||
@@ -782,6 +841,14 @@ body {
|
|||||||
color: #1976d2;
|
color: #1976d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.day-events {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.event-indicators {
|
.event-indicators {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -789,6 +856,22 @@ body {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-events-indicator {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 2px 4px;
|
||||||
|
text-align: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-events-indicator:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
.event-box {
|
.event-box {
|
||||||
/* Background color will be set inline via style attribute */
|
/* Background color will be set inline via style attribute */
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
Reference in New Issue
Block a user