Remove the original create_event_modal.rs and rename create_event_modal_v2.rs to complete the modal migration started earlier. This eliminates duplicate code and consolidates to a single, clean event modal implementation. Changes: - Remove original create_event_modal.rs (2,300+ lines) - Rename create_event_modal_v2.rs → create_event_modal.rs - Update component/function names: CreateEventModalV2 → CreateEventModal - Fix all imports in app.rs and calendar.rs - Add missing to_create_event_params() method to EventCreationData - Resolve EditAction type conflicts between modules - Clean up duplicate types and unused imports - Maintain backwards compatibility with EventCreationData export Result: -2440 lines, +160 lines - massive code cleanup with zero functionality loss. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
528 lines
21 KiB
Rust
528 lines
21 KiB
Rust
use crate::components::{
|
|
CalendarHeader, CreateEventModal, EventCreationData, EventModal, MonthView, ViewMode, WeekView,
|
|
};
|
|
use crate::models::ical::VEvent;
|
|
use crate::services::{calendar_service::UserInfo, CalendarService};
|
|
use chrono::{Datelike, Duration, Local, NaiveDate};
|
|
use gloo_storage::{LocalStorage, Storage};
|
|
use std::collections::HashMap;
|
|
use web_sys::MouseEvent;
|
|
use yew::prelude::*;
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct CalendarProps {
|
|
#[prop_or_default]
|
|
pub user_info: Option<UserInfo>,
|
|
#[prop_or_default]
|
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, VEvent)>>,
|
|
#[prop_or_default]
|
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, NaiveDate)>>,
|
|
#[prop_or_default]
|
|
pub view: ViewMode,
|
|
#[prop_or_default]
|
|
pub on_create_event_request: Option<Callback<EventCreationData>>,
|
|
#[prop_or_default]
|
|
pub on_event_update_request: Option<
|
|
Callback<(
|
|
VEvent,
|
|
chrono::NaiveDateTime,
|
|
chrono::NaiveDateTime,
|
|
bool,
|
|
Option<chrono::DateTime<chrono::Utc>>,
|
|
Option<String>,
|
|
Option<String>,
|
|
)>,
|
|
>,
|
|
#[prop_or_default]
|
|
pub context_menus_open: bool,
|
|
}
|
|
|
|
#[function_component]
|
|
pub fn Calendar(props: &CalendarProps) -> Html {
|
|
let today = Local::now().date_naive();
|
|
|
|
// Event management state
|
|
let events = use_state(|| HashMap::<NaiveDate, Vec<VEvent>>::new());
|
|
let loading = use_state(|| true);
|
|
let error = use_state(|| None::<String>);
|
|
let refreshing_event_uid = use_state(|| None::<String>);
|
|
// 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
|
|
} else {
|
|
today
|
|
}
|
|
} else {
|
|
// 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::<VEvent>);
|
|
|
|
// State for create event modal
|
|
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
|
|
}
|
|
});
|
|
|
|
// Fetch events when current_date changes
|
|
{
|
|
let events = events.clone();
|
|
let loading = loading.clone();
|
|
let error = error.clone();
|
|
let current_date = current_date.clone();
|
|
|
|
use_effect_with((*current_date, props.view.clone()), move |(date, _view)| {
|
|
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
|
|
let date = *date; // Clone the date to avoid lifetime issues
|
|
|
|
if let Some(token) = auth_token {
|
|
let events = events.clone();
|
|
let loading = loading.clone();
|
|
let error = error.clone();
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let calendar_service = CalendarService::new();
|
|
|
|
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()
|
|
};
|
|
|
|
let current_year = date.year();
|
|
let current_month = date.month();
|
|
|
|
match calendar_service
|
|
.fetch_events_for_month_vevent(
|
|
&token,
|
|
&password,
|
|
current_year,
|
|
current_month,
|
|
)
|
|
.await
|
|
{
|
|
Ok(vevents) => {
|
|
let grouped_events = CalendarService::group_events_by_date(vevents);
|
|
events.set(grouped_events);
|
|
loading.set(false);
|
|
}
|
|
Err(err) => {
|
|
error.set(Some(format!("Failed to load events: {}", err)));
|
|
loading.set(false);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
loading.set(false);
|
|
error.set(Some("No authentication token found".to_string()));
|
|
}
|
|
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
// Handle event click to refresh individual events
|
|
let on_event_click = {
|
|
let events = events.clone();
|
|
let refreshing_event_uid = refreshing_event_uid.clone();
|
|
|
|
Callback::from(move |event: VEvent| {
|
|
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
|
|
|
|
if let Some(token) = auth_token {
|
|
let events = events.clone();
|
|
let refreshing_event_uid = refreshing_event_uid.clone();
|
|
let uid = event.uid.clone();
|
|
|
|
refreshing_event_uid.set(Some(uid.clone()));
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let calendar_service = CalendarService::new();
|
|
|
|
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()
|
|
};
|
|
|
|
match calendar_service
|
|
.refresh_event(&token, &password, &uid)
|
|
.await
|
|
{
|
|
Ok(Some(refreshed_event)) => {
|
|
let refreshed_vevent = refreshed_event;
|
|
let mut updated_events = (*events).clone();
|
|
|
|
for (_, day_events) in updated_events.iter_mut() {
|
|
day_events.retain(|e| e.uid != uid);
|
|
}
|
|
|
|
if refreshed_vevent.rrule.is_some() {
|
|
let new_occurrences =
|
|
CalendarService::expand_recurring_events(vec![
|
|
refreshed_vevent.clone(),
|
|
]);
|
|
|
|
for occurrence in new_occurrences {
|
|
let date = occurrence.get_date();
|
|
updated_events
|
|
.entry(date)
|
|
.or_insert_with(Vec::new)
|
|
.push(occurrence);
|
|
}
|
|
} else {
|
|
let date = refreshed_vevent.get_date();
|
|
updated_events
|
|
.entry(date)
|
|
.or_insert_with(Vec::new)
|
|
.push(refreshed_vevent);
|
|
}
|
|
|
|
events.set(updated_events);
|
|
}
|
|
Ok(None) => {
|
|
let mut updated_events = (*events).clone();
|
|
for (_, day_events) in updated_events.iter_mut() {
|
|
day_events.retain(|e| e.uid != uid);
|
|
}
|
|
events.set(updated_events);
|
|
}
|
|
Err(_err) => {}
|
|
}
|
|
|
|
refreshing_event_uid.set(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_selected, new_display) = match view {
|
|
ViewMode::Month => {
|
|
// 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)
|
|
}
|
|
};
|
|
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_selected, new_display) = match view {
|
|
ViewMode::Month => {
|
|
// 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)
|
|
}
|
|
};
|
|
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_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
|
|
};
|
|
selected_date.set(new_selected);
|
|
current_date.set(new_display);
|
|
let _ = LocalStorage::set(
|
|
"calendar_selected_date",
|
|
new_selected.format("%Y-%m-%d").to_string(),
|
|
);
|
|
})
|
|
};
|
|
|
|
// 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();
|
|
let create_event_data = create_event_data.clone();
|
|
Callback::from(
|
|
move |(_date, start_datetime, end_datetime): (
|
|
NaiveDate,
|
|
chrono::NaiveDateTime,
|
|
chrono::NaiveDateTime,
|
|
)| {
|
|
// For drag-to-create, we don't need the temporary event approach
|
|
// Instead, just pass the local times directly via initial_time props
|
|
create_event_data.set(Some((
|
|
start_datetime.date(),
|
|
start_datetime.time(),
|
|
end_datetime.time(),
|
|
)));
|
|
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,
|
|
preserve_rrule,
|
|
until_date,
|
|
update_scope,
|
|
occurrence_date,
|
|
): (
|
|
VEvent,
|
|
chrono::NaiveDateTime,
|
|
chrono::NaiveDateTime,
|
|
bool,
|
|
Option<chrono::DateTime<chrono::Utc>>,
|
|
Option<String>,
|
|
Option<String>,
|
|
)| {
|
|
if let Some(callback) = &on_event_update_request {
|
|
callback.emit((
|
|
event,
|
|
new_start,
|
|
new_end,
|
|
preserve_rrule,
|
|
until_date,
|
|
update_scope,
|
|
occurrence_date,
|
|
));
|
|
}
|
|
},
|
|
)
|
|
};
|
|
|
|
html! {
|
|
<div class={classes!("calendar", match props.view { ViewMode::Week => Some("week-view"), _ => None })}>
|
|
<CalendarHeader
|
|
current_date={*current_date}
|
|
view_mode={props.view.clone()}
|
|
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)}
|
|
/>
|
|
|
|
{
|
|
if *loading {
|
|
html! {
|
|
<div class="calendar-loading">
|
|
<p>{"Loading calendar events..."}</p>
|
|
</div>
|
|
}
|
|
} else if let Some(err) = (*error).clone() {
|
|
html! {
|
|
<div class="calendar-error">
|
|
<p>{format!("Error: {}", err)}</p>
|
|
</div>
|
|
}
|
|
} else {
|
|
match props.view {
|
|
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={(*events).clone()}
|
|
on_event_click={on_event_click.clone()}
|
|
refreshing_event_uid={(*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
|
|
current_date={*current_date}
|
|
today={today}
|
|
events={(*events).clone()}
|
|
on_event_click={on_event_click.clone()}
|
|
refreshing_event_uid={(*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()}
|
|
on_create_event={Some(on_create_event)}
|
|
on_create_event_request={props.on_create_event_request.clone()}
|
|
on_event_update={Some(on_event_update)}
|
|
context_menus_open={props.context_menus_open}
|
|
time_increment={*time_increment}
|
|
/>
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Event details modal
|
|
<EventModal
|
|
event={(*selected_event).clone()}
|
|
on_close={{
|
|
let selected_event_clone = selected_event.clone();
|
|
Callback::from(move |_| {
|
|
selected_event_clone.set(None);
|
|
})
|
|
}}
|
|
/>
|
|
|
|
// Create event modal
|
|
<CreateEventModal
|
|
is_open={*show_create_modal}
|
|
selected_date={create_event_data.as_ref().map(|(date, _, _)| *date)}
|
|
event_to_edit={None}
|
|
available_calendars={props.user_info.as_ref().map(|info| info.calendars.clone()).unwrap_or_default()}
|
|
initial_start_time={create_event_data.as_ref().map(|(_, start_time, _)| *start_time)}
|
|
initial_end_time={create_event_data.as_ref().map(|(_, _, end_time)| *end_time)}
|
|
on_close={{
|
|
let show_create_modal = show_create_modal.clone();
|
|
let create_event_data = create_event_data.clone();
|
|
Callback::from(move |_| {
|
|
show_create_modal.set(false);
|
|
create_event_data.set(None);
|
|
})
|
|
}}
|
|
on_create={{
|
|
let show_create_modal = show_create_modal.clone();
|
|
let create_event_data = create_event_data.clone();
|
|
let on_create_event_request = props.on_create_event_request.clone();
|
|
Callback::from(move |event_data: EventCreationData| {
|
|
show_create_modal.set(false);
|
|
create_event_data.set(None);
|
|
|
|
// Emit the create event request to parent
|
|
if let Some(callback) = &on_create_event_request {
|
|
callback.emit(event_data);
|
|
}
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
}
|
|
}
|