- Created calendar-models/ shared library with RFC 5545-compliant VEvent structures - Migrated backend to use shared VEvent with proper field mappings (dtstart/dtend, rrule, exdate, etc.) - Converted CalDAV client to parse into VEvent structures with structured types - Updated all CRUD handlers to use VEvent with CalendarUser, Attendee, VAlarm types - Restructured project as Cargo workspace with frontend/, backend/, calendar-models/ - Updated Trunk configuration for new directory structure - Fixed all compilation errors and field references throughout codebase - Updated documentation and build instructions for workspace structure 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
316 lines
14 KiB
Rust
316 lines
14 KiB
Rust
use yew::prelude::*;
|
|
use chrono::{Datelike, Local, NaiveDate, Duration};
|
|
use std::collections::HashMap;
|
|
use web_sys::MouseEvent;
|
|
use crate::services::calendar_service::UserInfo;
|
|
use crate::models::ical::VEvent;
|
|
use crate::components::{EventModal, ViewMode, CalendarHeader, MonthView, WeekView, CreateEventModal, EventCreationData};
|
|
use gloo_storage::{LocalStorage, Storage};
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct CalendarProps {
|
|
#[prop_or_default]
|
|
pub events: HashMap<NaiveDate, Vec<VEvent>>,
|
|
pub on_event_click: Callback<VEvent>,
|
|
#[prop_or_default]
|
|
pub refreshing_event_uid: Option<String>,
|
|
#[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>>)>>,
|
|
#[prop_or_default]
|
|
pub context_menus_open: bool,
|
|
}
|
|
|
|
#[function_component]
|
|
pub fn Calendar(props: &CalendarProps) -> Html {
|
|
let today = Local::now().date_naive();
|
|
// 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
|
|
}
|
|
});
|
|
|
|
// 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): (VEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)| {
|
|
if let Some(callback) = &on_event_update_request {
|
|
callback.emit((event, new_start, new_end, preserve_rrule, until_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)}
|
|
/>
|
|
|
|
{
|
|
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={props.events.clone()}
|
|
on_event_click={props.on_event_click.clone()}
|
|
refreshing_event_uid={props.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={props.events.clone()}
|
|
on_event_click={props.on_event_click.clone()}
|
|
refreshing_event_uid={props.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>
|
|
}
|
|
} |