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:
Connor Johnstone
2025-09-03 18:22:52 -04:00
parent 289284a532
commit 8caa1f45ae
16 changed files with 1207 additions and 18 deletions

View File

@@ -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>
}

View File

@@ -14,6 +14,8 @@ pub struct CalendarProps {
#[prop_or_default]
pub user_info: Option<UserInfo>,
#[prop_or_default]
pub external_calendar_events: Vec<VEvent>,
#[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)>>,
@@ -101,10 +103,13 @@ pub fn Calendar(props: &CalendarProps) -> Html {
let loading = loading.clone();
let error = error.clone();
let current_date = current_date.clone();
let external_events = props.external_calendar_events.clone(); // Clone before the effect
let view = props.view.clone(); // Clone before the effect
use_effect_with((*current_date, props.view.clone()), move |(date, _view)| {
use_effect_with((*current_date, view.clone(), external_events.len()), move |(date, _view, _external_len)| {
let auth_token: Option<String> = LocalStorage::get("auth_token").ok();
let date = *date; // Clone the date to avoid lifetime issues
let external_events = external_events.clone(); // Clone external events to avoid lifetime issues
if let Some(token) = auth_token {
let events = events.clone();
@@ -141,7 +146,11 @@ pub fn Calendar(props: &CalendarProps) -> Html {
.await
{
Ok(vevents) => {
let grouped_events = CalendarService::group_events_by_date(vevents);
// Combine regular events with external calendar events
let mut all_events = vevents;
all_events.extend(external_events);
let grouped_events = CalendarService::group_events_by_date(all_events);
events.set(grouped_events);
loading.set(false);
}

View File

@@ -0,0 +1,193 @@
use web_sys::HtmlInputElement;
use yew::prelude::*;
use crate::services::calendar_service::CalendarService;
#[derive(Properties, PartialEq)]
pub struct ExternalCalendarModalProps {
pub is_open: bool,
pub on_close: Callback<()>,
pub on_success: Callback<()>,
}
#[function_component(ExternalCalendarModal)]
pub fn external_calendar_modal(props: &ExternalCalendarModalProps) -> Html {
let name_ref = use_node_ref();
let url_ref = use_node_ref();
let color_ref = use_node_ref();
let is_loading = use_state(|| false);
let error_message = use_state(|| None::<String>);
let on_submit = {
let name_ref = name_ref.clone();
let url_ref = url_ref.clone();
let color_ref = color_ref.clone();
let is_loading = is_loading.clone();
let error_message = error_message.clone();
let on_close = props.on_close.clone();
let on_success = props.on_success.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
let name = name_ref
.cast::<HtmlInputElement>()
.map(|input| input.value())
.unwrap_or_default()
.trim()
.to_string();
let url = url_ref
.cast::<HtmlInputElement>()
.map(|input| input.value())
.unwrap_or_default()
.trim()
.to_string();
let color = color_ref
.cast::<HtmlInputElement>()
.map(|input| input.value())
.unwrap_or_else(|| "#4285f4".to_string());
if name.is_empty() {
error_message.set(Some("Calendar name is required".to_string()));
return;
}
if url.is_empty() {
error_message.set(Some("Calendar URL is required".to_string()));
return;
}
// Basic URL validation
if !url.starts_with("http://") && !url.starts_with("https://") {
error_message.set(Some("Please enter a valid HTTP or HTTPS URL".to_string()));
return;
}
error_message.set(None);
is_loading.set(true);
let is_loading = is_loading.clone();
let error_message = error_message.clone();
let on_close = on_close.clone();
let on_success = on_success.clone();
wasm_bindgen_futures::spawn_local(async move {
match CalendarService::create_external_calendar(&name, &url, &color).await {
Ok(_) => {
is_loading.set(false);
on_success.emit(());
on_close.emit(());
}
Err(e) => {
is_loading.set(false);
error_message.set(Some(format!("Failed to add calendar: {}", e)));
}
}
});
})
};
let on_cancel = {
let on_close = props.on_close.clone();
Callback::from(move |_| {
on_close.emit(());
})
};
let on_cancel_clone = on_cancel.clone();
if !props.is_open {
return html! {};
}
html! {
<div class="modal-overlay" onclick={on_cancel_clone}>
<div class="modal-content" onclick={Callback::from(|e: MouseEvent| e.stop_propagation())}>
<div class="modal-header">
<h3>{"Add External Calendar"}</h3>
<button class="modal-close" onclick={on_cancel.clone()}>{"×"}</button>
</div>
<form onsubmit={on_submit}>
<div class="modal-body">
{
if let Some(error) = (*error_message).as_ref() {
html! {
<div class="error-message">
{error}
</div>
}
} else {
html! {}
}
}
<div class="form-group">
<label for="external-calendar-name">{"Calendar Name"}</label>
<input
ref={name_ref}
id="external-calendar-name"
type="text"
placeholder="My External Calendar"
disabled={*is_loading}
required={true}
/>
</div>
<div class="form-group">
<label for="external-calendar-url">{"ICS URL"}</label>
<input
ref={url_ref}
id="external-calendar-url"
type="url"
placeholder="https://example.com/calendar.ics"
disabled={*is_loading}
required={true}
/>
<small class="form-help">
{"Enter the public ICS URL for the calendar you want to add"}
</small>
</div>
<div class="form-group">
<label for="external-calendar-color">{"Color"}</label>
<input
ref={color_ref}
id="external-calendar-color"
type="color"
value="#4285f4"
disabled={*is_loading}
/>
</div>
</div>
<div class="modal-actions">
<button
type="button"
class="btn btn-secondary"
onclick={on_cancel}
disabled={*is_loading}
>
{"Cancel"}
</button>
<button
type="submit"
class="btn btn-primary"
disabled={*is_loading}
>
{
if *is_loading {
"Adding..."
} else {
"Add Calendar"
}
}
</button>
</div>
</form>
</div>
</div>
}
}

View File

@@ -8,6 +8,7 @@ pub mod create_event_modal;
pub mod event_context_menu;
pub mod event_form;
pub mod event_modal;
pub mod external_calendar_modal;
pub mod login;
pub mod month_view;
pub mod recurring_edit_modal;
@@ -26,6 +27,7 @@ pub use create_event_modal::CreateEventModal;
pub use event_form::EventCreationData;
pub use event_context_menu::{DeleteAction, EditAction, EventContextMenu};
pub use event_modal::EventModal;
pub use external_calendar_modal::ExternalCalendarModal;
pub use login::Login;
pub use month_view::MonthView;
pub use recurring_edit_modal::{RecurringEditAction, RecurringEditModal};

View File

@@ -20,6 +20,8 @@ pub struct RouteHandlerProps {
pub user_info: Option<UserInfo>,
pub on_login: Callback<String>,
#[prop_or_default]
pub external_calendar_events: Vec<VEvent>,
#[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, chrono::NaiveDate)>>,
@@ -48,6 +50,7 @@ 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 external_calendar_events = props.external_calendar_events.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();
@@ -60,6 +63,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
let auth_token = auth_token.clone();
let user_info = user_info.clone();
let on_login = on_login.clone();
let external_calendar_events = external_calendar_events.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();
@@ -87,6 +91,7 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
html! {
<CalendarView
user_info={user_info}
external_calendar_events={external_calendar_events}
on_event_context_menu={on_event_context_menu}
on_calendar_context_menu={on_calendar_context_menu}
view={view}
@@ -108,6 +113,8 @@ pub fn route_handler(props: &RouteHandlerProps) -> Html {
pub struct CalendarViewProps {
pub user_info: Option<UserInfo>,
#[prop_or_default]
pub external_calendar_events: Vec<VEvent>,
#[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, chrono::NaiveDate)>>,
@@ -139,6 +146,7 @@ pub fn calendar_view(props: &CalendarViewProps) -> Html {
<div class="calendar-view">
<Calendar
user_info={props.user_info.clone()}
external_calendar_events={props.external_calendar_events.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()}

View File

@@ -1,5 +1,5 @@
use crate::components::CalendarListItem;
use crate::services::calendar_service::UserInfo;
use crate::services::calendar_service::{UserInfo, ExternalCalendar};
use web_sys::HtmlSelectElement;
use yew::prelude::*;
use yew_router::prelude::*;
@@ -101,6 +101,9 @@ pub struct SidebarProps {
pub user_info: Option<UserInfo>,
pub on_logout: Callback<()>,
pub on_create_calendar: Callback<()>,
pub on_create_external_calendar: Callback<()>,
pub external_calendars: Vec<ExternalCalendar>,
pub on_external_calendar_toggle: Callback<i32>,
pub color_picker_open: Option<String>,
pub on_color_change: Callback<(String, String)>,
pub on_color_picker_toggle: Callback<String>,
@@ -206,10 +209,59 @@ pub fn sidebar(props: &SidebarProps) -> Html {
html! {}
}
}
// External calendars section
<div class="external-calendar-list">
<h3>{"External Calendars"}</h3>
{
if !props.external_calendars.is_empty() {
html! {
<ul class="external-calendar-items">
{
props.external_calendars.iter().map(|cal| {
let on_toggle = {
let on_external_calendar_toggle = props.on_external_calendar_toggle.clone();
let cal_id = cal.id;
Callback::from(move |_| {
on_external_calendar_toggle.emit(cal_id);
})
};
html! {
<li class="external-calendar-item">
<div class="external-calendar-info">
<input
type="checkbox"
checked={cal.is_visible}
onchange={on_toggle}
/>
<span
class="external-calendar-color"
style={format!("background-color: {}", cal.color)}
/>
<span class="external-calendar-name">{&cal.name}</span>
<span class="external-calendar-indicator">{"📅"}</span>
</div>
</li>
}
}).collect::<Html>()
}
</ul>
}
} else {
html! {}
}
}
</div>
<div class="sidebar-footer">
<button onclick={props.on_create_calendar.reform(|_| ())} class="create-calendar-button">
{"+ Create Calendar"}
</button>
<button onclick={props.on_create_external_calendar.reform(|_| ())} class="create-external-calendar-button">
{"+ Add External Calendar"}
</button>
<div class="view-selector">
<select class="view-selector-dropdown" onchange={on_view_change}>

View File

@@ -1,4 +1,5 @@
use chrono::{DateTime, Datelike, Duration, NaiveDate, TimeZone, Utc, Weekday};
use gloo_storage::{LocalStorage, Storage};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use wasm_bindgen::JsCast;
@@ -1854,4 +1855,256 @@ impl CalendarService {
None
}
// ==================== EXTERNAL CALENDAR METHODS ====================
pub async fn get_external_calendars() -> Result<Vec<ExternalCalendar>, String> {
let token = LocalStorage::get::<String>("auth_token")
.map_err(|_| "No authentication token found".to_string())?;
let window = web_sys::window().ok_or("No global window exists")?;
let opts = RequestInit::new();
opts.set_method("GET");
opts.set_mode(RequestMode::Cors);
let service = Self::new();
let url = format!("{}/external-calendars", service.base_url);
let request = Request::new_with_str_and_init(&url, &opts)
.map_err(|e| format!("Request creation failed: {:?}", e))?;
request
.headers()
.set("Authorization", &format!("Bearer {}", token))
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
let resp_value = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|e| format!("Request failed: {:?}", e))?;
let resp: Response = resp_value
.dyn_into()
.map_err(|e| format!("Response casting failed: {:?}", e))?;
if !resp.ok() {
return Err(format!("HTTP error: {}", resp.status()));
}
let json = JsFuture::from(resp.json().unwrap())
.await
.map_err(|e| format!("JSON parsing failed: {:?}", e))?;
let external_calendars: Vec<ExternalCalendar> = serde_wasm_bindgen::from_value(json)
.map_err(|e| format!("Deserialization failed: {:?}", e))?;
Ok(external_calendars)
}
pub async fn create_external_calendar(name: &str, url: &str, color: &str) -> Result<ExternalCalendar, String> {
let token = LocalStorage::get::<String>("auth_token")
.map_err(|_| "No authentication token found".to_string())?;
let window = web_sys::window().ok_or("No global window exists")?;
let opts = RequestInit::new();
opts.set_method("POST");
opts.set_mode(RequestMode::Cors);
let body = serde_json::json!({
"name": name,
"url": url,
"color": color
});
let service = Self::new();
let body_string = serde_json::to_string(&body)
.map_err(|e| format!("JSON serialization failed: {}", e))?;
opts.set_body(&body_string.into());
let url = format!("{}/external-calendars", service.base_url);
let request = Request::new_with_str_and_init(&url, &opts)
.map_err(|e| format!("Request creation failed: {:?}", e))?;
request
.headers()
.set("Authorization", &format!("Bearer {}", token))
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
request
.headers()
.set("Content-Type", "application/json")
.map_err(|e| format!("Content-Type header setting failed: {:?}", e))?;
let resp_value = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|e| format!("Request failed: {:?}", e))?;
let resp: Response = resp_value
.dyn_into()
.map_err(|e| format!("Response casting failed: {:?}", e))?;
if !resp.ok() {
return Err(format!("HTTP error: {}", resp.status()));
}
let json = JsFuture::from(resp.json().unwrap())
.await
.map_err(|e| format!("JSON parsing failed: {:?}", e))?;
let external_calendar: ExternalCalendar = serde_wasm_bindgen::from_value(json)
.map_err(|e| format!("Deserialization failed: {:?}", e))?;
Ok(external_calendar)
}
pub async fn update_external_calendar(
id: i32,
name: &str,
url: &str,
color: &str,
is_visible: bool,
) -> Result<(), String> {
let token = LocalStorage::get::<String>("auth_token")
.map_err(|_| "No authentication token found".to_string())?;
let window = web_sys::window().ok_or("No global window exists")?;
let opts = RequestInit::new();
opts.set_method("POST");
opts.set_mode(RequestMode::Cors);
let body = serde_json::json!({
"name": name,
"url": url,
"color": color,
"is_visible": is_visible
});
let service = Self::new();
let body_string = serde_json::to_string(&body)
.map_err(|e| format!("JSON serialization failed: {}", e))?;
opts.set_body(&body_string.into());
let url = format!("{}/external-calendars/{}", service.base_url, id);
let request = Request::new_with_str_and_init(&url, &opts)
.map_err(|e| format!("Request creation failed: {:?}", e))?;
request
.headers()
.set("Authorization", &format!("Bearer {}", token))
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
request
.headers()
.set("Content-Type", "application/json")
.map_err(|e| format!("Content-Type header setting failed: {:?}", e))?;
let resp_value = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|e| format!("Request failed: {:?}", e))?;
let resp: Response = resp_value
.dyn_into()
.map_err(|e| format!("Response casting failed: {:?}", e))?;
if !resp.ok() {
return Err(format!("HTTP error: {}", resp.status()));
}
Ok(())
}
pub async fn delete_external_calendar(id: i32) -> Result<(), String> {
let token = LocalStorage::get::<String>("auth_token")
.map_err(|_| "No authentication token found".to_string())?;
let window = web_sys::window().ok_or("No global window exists")?;
let opts = RequestInit::new();
opts.set_method("DELETE");
opts.set_mode(RequestMode::Cors);
let service = Self::new();
let url = format!("{}/external-calendars/{}", service.base_url, id);
let request = Request::new_with_str_and_init(&url, &opts)
.map_err(|e| format!("Request creation failed: {:?}", e))?;
request
.headers()
.set("Authorization", &format!("Bearer {}", token))
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
let resp_value = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|e| format!("Request failed: {:?}", e))?;
let resp: Response = resp_value
.dyn_into()
.map_err(|e| format!("Response casting failed: {:?}", e))?;
if !resp.ok() {
return Err(format!("HTTP error: {}", resp.status()));
}
Ok(())
}
pub async fn fetch_external_calendar_events(id: i32) -> Result<Vec<VEvent>, String> {
let token = LocalStorage::get::<String>("auth_token")
.map_err(|_| "No authentication token found".to_string())?;
let window = web_sys::window().ok_or("No global window exists")?;
let opts = RequestInit::new();
opts.set_method("GET");
opts.set_mode(RequestMode::Cors);
let service = Self::new();
let url = format!("{}/external-calendars/{}/events", service.base_url, id);
let request = Request::new_with_str_and_init(&url, &opts)
.map_err(|e| format!("Request creation failed: {:?}", e))?;
request
.headers()
.set("Authorization", &format!("Bearer {}", token))
.map_err(|e| format!("Authorization header setting failed: {:?}", e))?;
let resp_value = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|e| format!("Request failed: {:?}", e))?;
let resp: Response = resp_value
.dyn_into()
.map_err(|e| format!("Response casting failed: {:?}", e))?;
if !resp.ok() {
return Err(format!("HTTP error: {}", resp.status()));
}
let json = JsFuture::from(resp.json().unwrap())
.await
.map_err(|e| format!("JSON parsing failed: {:?}", e))?;
#[derive(Deserialize)]
struct ExternalCalendarEventsResponse {
events: Vec<VEvent>,
}
let response: ExternalCalendarEventsResponse = serde_wasm_bindgen::from_value(json)
.map_err(|e| format!("Deserialization failed: {:?}", e))?;
Ok(response.events)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ExternalCalendar {
pub id: i32,
pub name: String,
pub url: String,
pub color: String,
pub is_visible: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
pub last_fetched: Option<chrono::DateTime<chrono::Utc>>,
}