- Add drag-to-move event handlers to existing events in week view - Extend drag state management to support both create and move operations - Implement visual feedback with event preview during drag and hidden original - Calculate new start/end times while preserving event duration - Add CalDAV server update integration via calendar service - Wire event update callbacks through component hierarchy (WeekView → Calendar → RouteHandler → App) - Preserve all original event properties (title, description, location, reminders, etc.) - Handle timezone conversion from local to UTC for server storage - Add error handling with user feedback and success confirmation - Include moving event CSS styling with enhanced visual feedback 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
295 lines
12 KiB
Rust
295 lines
12 KiB
Rust
use yew::prelude::*;
|
|
use yew_router::prelude::*;
|
|
use crate::components::{Login, ViewMode};
|
|
use crate::services::calendar_service::{UserInfo, CalendarEvent};
|
|
|
|
#[derive(Clone, Routable, PartialEq)]
|
|
pub enum Route {
|
|
#[at("/")]
|
|
Home,
|
|
#[at("/login")]
|
|
Login,
|
|
#[at("/calendar")]
|
|
Calendar,
|
|
}
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct RouteHandlerProps {
|
|
pub auth_token: Option<String>,
|
|
pub user_info: Option<UserInfo>,
|
|
pub on_login: Callback<String>,
|
|
#[prop_or_default]
|
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
|
#[prop_or_default]
|
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>,
|
|
#[prop_or_default]
|
|
pub view: ViewMode,
|
|
#[prop_or_default]
|
|
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)]
|
|
pub fn route_handler(props: &RouteHandlerProps) -> Html {
|
|
let auth_token = props.auth_token.clone();
|
|
let user_info = props.user_info.clone();
|
|
let on_login = props.on_login.clone();
|
|
let on_event_context_menu = props.on_event_context_menu.clone();
|
|
let on_calendar_context_menu = props.on_calendar_context_menu.clone();
|
|
let view = props.view.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! {
|
|
<Switch<Route> render={move |route| {
|
|
let auth_token = auth_token.clone();
|
|
let user_info = user_info.clone();
|
|
let on_login = on_login.clone();
|
|
let on_event_context_menu = on_event_context_menu.clone();
|
|
let on_calendar_context_menu = on_calendar_context_menu.clone();
|
|
let view = view.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 {
|
|
Route::Home => {
|
|
if auth_token.is_some() {
|
|
html! { <Redirect<Route> to={Route::Calendar}/> }
|
|
} else {
|
|
html! { <Redirect<Route> to={Route::Login}/> }
|
|
}
|
|
}
|
|
Route::Login => {
|
|
if auth_token.is_some() {
|
|
html! { <Redirect<Route> to={Route::Calendar}/> }
|
|
} else {
|
|
html! { <Login {on_login} /> }
|
|
}
|
|
}
|
|
Route::Calendar => {
|
|
if auth_token.is_some() {
|
|
html! {
|
|
<CalendarView
|
|
user_info={user_info}
|
|
on_event_context_menu={on_event_context_menu}
|
|
on_calendar_context_menu={on_calendar_context_menu}
|
|
view={view}
|
|
on_create_event_request={on_create_event_request}
|
|
on_event_update_request={on_event_update_request}
|
|
context_menus_open={context_menus_open}
|
|
/>
|
|
}
|
|
} else {
|
|
html! { <Redirect<Route> to={Route::Login}/> }
|
|
}
|
|
}
|
|
}
|
|
}} />
|
|
}
|
|
}
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct CalendarViewProps {
|
|
pub user_info: Option<UserInfo>,
|
|
#[prop_or_default]
|
|
pub on_event_context_menu: Option<Callback<(web_sys::MouseEvent, CalendarEvent)>>,
|
|
#[prop_or_default]
|
|
pub on_calendar_context_menu: Option<Callback<(web_sys::MouseEvent, chrono::NaiveDate)>>,
|
|
#[prop_or_default]
|
|
pub view: ViewMode,
|
|
#[prop_or_default]
|
|
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 crate::services::CalendarService;
|
|
use crate::components::Calendar;
|
|
use std::collections::HashMap;
|
|
use chrono::{Local, NaiveDate, Datelike};
|
|
|
|
#[function_component(CalendarView)]
|
|
pub fn calendar_view(props: &CalendarViewProps) -> Html {
|
|
let events = use_state(|| HashMap::<NaiveDate, Vec<CalendarEvent>>::new());
|
|
let loading = use_state(|| true);
|
|
let error = use_state(|| None::<String>);
|
|
let refreshing_event = use_state(|| None::<String>);
|
|
|
|
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
|
|
|
|
|
|
let today = Local::now().date_naive();
|
|
let current_year = today.year();
|
|
let current_month = today.month();
|
|
|
|
let on_event_click = {
|
|
let events = events.clone();
|
|
let refreshing_event = refreshing_event.clone();
|
|
let auth_token = auth_token.clone();
|
|
|
|
Callback::from(move |event: CalendarEvent| {
|
|
if let Some(token) = auth_token.clone() {
|
|
let events = events.clone();
|
|
let refreshing_event = refreshing_event.clone();
|
|
let uid = event.uid.clone();
|
|
|
|
refreshing_event.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 mut updated_events = (*events).clone();
|
|
|
|
for (_, day_events) in updated_events.iter_mut() {
|
|
day_events.retain(|e| e.uid != uid);
|
|
}
|
|
|
|
if refreshed_event.recurrence_rule.is_some() {
|
|
let new_occurrences = CalendarService::expand_recurring_events(vec![refreshed_event.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_event.get_date();
|
|
updated_events.entry(date)
|
|
.or_insert_with(Vec::new)
|
|
.push(refreshed_event);
|
|
}
|
|
|
|
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.set(None);
|
|
});
|
|
}
|
|
})
|
|
};
|
|
|
|
{
|
|
let events = events.clone();
|
|
let loading = loading.clone();
|
|
let error = error.clone();
|
|
let auth_token = auth_token.clone();
|
|
|
|
use_effect_with((), move |_| {
|
|
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()
|
|
};
|
|
|
|
match calendar_service.fetch_events_for_month(&token, &password, current_year, current_month).await {
|
|
Ok(calendar_events) => {
|
|
let grouped_events = CalendarService::group_events_by_date(calendar_events);
|
|
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()));
|
|
}
|
|
|
|
|| ()
|
|
});
|
|
}
|
|
|
|
html! {
|
|
<div class="calendar-view">
|
|
{
|
|
if *loading {
|
|
html! {
|
|
<div class="calendar-loading">
|
|
<p>{"Loading calendar events..."}</p>
|
|
</div>
|
|
}
|
|
} else if let Some(err) = (*error).clone() {
|
|
let dummy_callback = Callback::from(|_: CalendarEvent| {});
|
|
html! {
|
|
<div class="calendar-error">
|
|
<p>{format!("Error: {}", err)}</p>
|
|
<Calendar
|
|
events={HashMap::new()}
|
|
on_event_click={dummy_callback}
|
|
refreshing_event_uid={(*refreshing_event).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()}
|
|
view={props.view.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>
|
|
}
|
|
} else {
|
|
html! {
|
|
<Calendar
|
|
events={(*events).clone()}
|
|
on_event_click={on_event_click}
|
|
refreshing_event_uid={(*refreshing_event).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()}
|
|
view={props.view.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>
|
|
}
|
|
} |