Add external calendars feature: display read-only ICS calendars alongside CalDAV calendars
- Database: Add external_calendars table with user relationships and CRUD operations - Backend: Implement REST API endpoints for external calendar management and ICS fetching - Frontend: Add external calendar modal, sidebar section with visibility toggles - Calendar integration: Merge external events with regular events in unified view - ICS parsing: Support multiple datetime formats, recurring events, and timezone handling - Authentication: Integrate with existing JWT token system for user-specific calendars - UI: Visual distinction with 📅 indicator and separate management section 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
use crate::components::{
|
||||
CalendarContextMenu, ContextMenu, CreateCalendarModal, CreateEventModal, DeleteAction,
|
||||
EditAction, EventContextMenu, EventCreationData, RouteHandler, Sidebar, Theme, ViewMode,
|
||||
EditAction, EventContextMenu, EventCreationData, ExternalCalendarModal, RouteHandler,
|
||||
Sidebar, Theme, ViewMode,
|
||||
};
|
||||
use crate::components::sidebar::{Style};
|
||||
use crate::models::ical::VEvent;
|
||||
use crate::services::{calendar_service::UserInfo, CalendarService};
|
||||
use crate::services::{calendar_service::{UserInfo, ExternalCalendar}, CalendarService};
|
||||
use chrono::NaiveDate;
|
||||
use gloo_storage::{LocalStorage, Storage};
|
||||
use wasm_bindgen::JsCast;
|
||||
@@ -73,6 +74,11 @@ pub fn App() -> Html {
|
||||
let _recurring_edit_modal_open = use_state(|| false);
|
||||
let _recurring_edit_event = use_state(|| -> Option<VEvent> { None });
|
||||
let _recurring_edit_data = use_state(|| -> Option<EventCreationData> { None });
|
||||
|
||||
// External calendar state
|
||||
let external_calendars = use_state(|| -> Vec<ExternalCalendar> { Vec::new() });
|
||||
let external_calendar_events = use_state(|| -> Vec<VEvent> { Vec::new() });
|
||||
let external_calendar_modal_open = use_state(|| false);
|
||||
|
||||
// Calendar view state - load from localStorage if available
|
||||
let current_view = use_state(|| {
|
||||
@@ -301,6 +307,50 @@ pub fn App() -> Html {
|
||||
});
|
||||
}
|
||||
|
||||
// Load external calendars when auth token is available
|
||||
{
|
||||
let auth_token = auth_token.clone();
|
||||
let external_calendars = external_calendars.clone();
|
||||
let external_calendar_events = external_calendar_events.clone();
|
||||
|
||||
use_effect_with((*auth_token).clone(), move |token| {
|
||||
if token.is_some() {
|
||||
let external_calendars = external_calendars.clone();
|
||||
let external_calendar_events = external_calendar_events.clone();
|
||||
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
// Load external calendars
|
||||
match CalendarService::get_external_calendars().await {
|
||||
Ok(calendars) => {
|
||||
external_calendars.set(calendars.clone());
|
||||
|
||||
// Load events for visible external calendars
|
||||
let mut all_events = Vec::new();
|
||||
for calendar in calendars {
|
||||
if calendar.is_visible {
|
||||
if let Ok(events) = CalendarService::fetch_external_calendar_events(calendar.id).await {
|
||||
all_events.extend(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
external_calendar_events.set(all_events);
|
||||
}
|
||||
Err(err) => {
|
||||
web_sys::console::log_1(
|
||||
&format!("Failed to load external calendars: {}", err).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
external_calendars.set(Vec::new());
|
||||
external_calendar_events.set(Vec::new());
|
||||
}
|
||||
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let on_outside_click = {
|
||||
let color_picker_open = color_picker_open.clone();
|
||||
let context_menu_open = context_menu_open.clone();
|
||||
@@ -924,6 +974,53 @@ pub fn App() -> Html {
|
||||
let create_modal_open = create_modal_open.clone();
|
||||
move |_| create_modal_open.set(true)
|
||||
})}
|
||||
on_create_external_calendar={Callback::from({
|
||||
let external_calendar_modal_open = external_calendar_modal_open.clone();
|
||||
move |_| external_calendar_modal_open.set(true)
|
||||
})}
|
||||
external_calendars={(*external_calendars).clone()}
|
||||
on_external_calendar_toggle={Callback::from({
|
||||
let external_calendars = external_calendars.clone();
|
||||
let external_calendar_events = external_calendar_events.clone();
|
||||
move |id: i32| {
|
||||
let external_calendars = external_calendars.clone();
|
||||
let external_calendar_events = external_calendar_events.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
// Find the calendar and toggle its visibility
|
||||
let mut calendars = (*external_calendars).clone();
|
||||
if let Some(calendar) = calendars.iter_mut().find(|c| c.id == id) {
|
||||
calendar.is_visible = !calendar.is_visible;
|
||||
|
||||
// Update on server
|
||||
if let Err(err) = CalendarService::update_external_calendar(
|
||||
calendar.id,
|
||||
&calendar.name,
|
||||
&calendar.url,
|
||||
&calendar.color,
|
||||
calendar.is_visible,
|
||||
).await {
|
||||
web_sys::console::log_1(
|
||||
&format!("Failed to update external calendar: {}", err).into(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
external_calendars.set(calendars.clone());
|
||||
|
||||
// Reload events for all visible external calendars
|
||||
let mut all_events = Vec::new();
|
||||
for cal in calendars {
|
||||
if cal.is_visible {
|
||||
if let Ok(events) = CalendarService::fetch_external_calendar_events(cal.id).await {
|
||||
all_events.extend(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
external_calendar_events.set(all_events);
|
||||
}
|
||||
});
|
||||
}
|
||||
})}
|
||||
color_picker_open={(*color_picker_open).clone()}
|
||||
on_color_change={on_color_change}
|
||||
on_color_picker_toggle={on_color_picker_toggle}
|
||||
@@ -941,6 +1038,7 @@ pub fn App() -> Html {
|
||||
auth_token={(*auth_token).clone()}
|
||||
user_info={(*user_info).clone()}
|
||||
on_login={on_login.clone()}
|
||||
external_calendar_events={(*external_calendar_events).clone()}
|
||||
on_event_context_menu={Some(on_event_context_menu.clone())}
|
||||
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
||||
view={(*current_view).clone()}
|
||||
@@ -1193,6 +1291,46 @@ pub fn App() -> Html {
|
||||
on_create={on_event_create}
|
||||
available_calendars={user_info.as_ref().map(|ui| ui.calendars.clone()).unwrap_or_default()}
|
||||
/>
|
||||
|
||||
<ExternalCalendarModal
|
||||
is_open={*external_calendar_modal_open}
|
||||
on_close={Callback::from({
|
||||
let external_calendar_modal_open = external_calendar_modal_open.clone();
|
||||
move |_| external_calendar_modal_open.set(false)
|
||||
})}
|
||||
on_success={Callback::from({
|
||||
let external_calendars = external_calendars.clone();
|
||||
let external_calendar_events = external_calendar_events.clone();
|
||||
move |_| {
|
||||
// Reload external calendars
|
||||
let external_calendars = external_calendars.clone();
|
||||
let external_calendar_events = external_calendar_events.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
match CalendarService::get_external_calendars().await {
|
||||
Ok(calendars) => {
|
||||
external_calendars.set(calendars.clone());
|
||||
|
||||
// Load events for visible external calendars
|
||||
let mut all_events = Vec::new();
|
||||
for calendar in calendars {
|
||||
if calendar.is_visible {
|
||||
if let Ok(events) = CalendarService::fetch_external_calendar_events(calendar.id).await {
|
||||
all_events.extend(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
external_calendar_events.set(all_events);
|
||||
}
|
||||
Err(err) => {
|
||||
web_sys::console::log_1(
|
||||
&format!("Failed to reload external calendars: {}", err).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user