Some checks failed
Build and Push Docker Image / docker (push) Failing after 1m7s
Moved event fetching logic from CalendarView to Calendar component to properly use the visible date range instead of hardcoded current month. The Calendar component already tracks the current visible date through navigation, so events now load correctly for August and other months when navigating. Changes: - Calendar component now manages its own events state and fetching - Event fetching responds to current_date changes from navigation - CalendarView simplified to just render Calendar component - Fixed cargo fmt/clippy formatting across codebase 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
537 lines
21 KiB
Rust
537 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);
|
|
}
|
|
})
|
|
}}
|
|
on_update={{
|
|
let show_create_modal = show_create_modal.clone();
|
|
let create_event_data = create_event_data.clone();
|
|
Callback::from(move |(_original_event, _updated_data): (VEvent, EventCreationData)| {
|
|
show_create_modal.set(false);
|
|
create_event_data.set(None);
|
|
// TODO: Handle actual event update
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
}
|
|
}
|