Implement comprehensive theme system with calendar view support

- Add 8 attractive themes (Default, Ocean, Forest, Sunset, Purple, Dark, Rose, Mint)
- Extend theme system to calendar header, month view, and week view components
- Add dynamic theme-aware event color palettes that change with selected theme
- Implement CSS custom properties for consistent theming across all components
- Add localStorage persistence for user theme preferences
- Create theme-aware calendar styling including day states, headers, and grid lines
- Optimize dark theme with proper contrast and readability improvements
- Add reactive event color system that updates when themes change

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-08-29 16:42:25 -04:00
parent 81805289e4
commit 4af4aafd98
11 changed files with 804 additions and 107 deletions

View File

@@ -2,10 +2,36 @@ use yew::prelude::*;
use yew_router::prelude::*;
use gloo_storage::{LocalStorage, Storage};
use web_sys::MouseEvent;
use crate::components::{Sidebar, ViewMode, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction};
use crate::components::{Sidebar, ViewMode, Theme, CreateCalendarModal, ContextMenu, EventContextMenu, CalendarContextMenu, CreateEventModal, EventCreationData, RouteHandler, EventStatus, EventClass, ReminderType, RecurrenceType, DeleteAction};
use crate::services::{CalendarService, calendar_service::{UserInfo, CalendarEvent}};
use chrono::NaiveDate;
fn get_theme_event_colors() -> Vec<String> {
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
if let Some(root) = document.document_element() {
if let Ok(Some(computed_style)) = window.get_computed_style(&root) {
if let Ok(colors_string) = computed_style.get_property_value("--event-colors") {
if !colors_string.is_empty() {
return colors_string
.split(',')
.map(|color| color.trim().to_string())
.filter(|color| !color.is_empty())
.collect();
}
}
}
}
}
}
vec![
"#3B82F6".to_string(), "#10B981".to_string(), "#F59E0B".to_string(), "#EF4444".to_string(),
"#8B5CF6".to_string(), "#06B6D4".to_string(), "#84CC16".to_string(), "#F97316".to_string(),
"#EC4899".to_string(), "#6366F1".to_string(), "#14B8A6".to_string(), "#F3B806".to_string(),
"#8B5A2B".to_string(), "#6B7280".to_string(), "#DC2626".to_string(), "#7C3AED".to_string()
]
}
#[function_component]
pub fn App() -> Html {
@@ -41,12 +67,17 @@ pub fn App() -> Html {
}
});
let available_colors = [
"#3B82F6", "#10B981", "#F59E0B", "#EF4444",
"#8B5CF6", "#06B6D4", "#84CC16", "#F97316",
"#EC4899", "#6366F1", "#14B8A6", "#F3B806",
"#8B5A2B", "#6B7280", "#DC2626", "#7C3AED"
];
// Theme state - load from localStorage if available
let current_theme = use_state(|| {
// Try to load saved theme from localStorage
if let Ok(saved_theme) = LocalStorage::get::<String>("calendar_theme") {
Theme::from_value(&saved_theme)
} else {
Theme::Default // Default theme
}
});
let available_colors = use_state(|| get_theme_event_colors());
let on_login = {
let auth_token = auth_token.clone();
@@ -80,6 +111,41 @@ pub fn App() -> Html {
})
};
let on_theme_change = {
let current_theme = current_theme.clone();
let available_colors = available_colors.clone();
Callback::from(move |new_theme: Theme| {
// Save theme to localStorage
let _ = LocalStorage::set("calendar_theme", new_theme.value());
// Apply theme to document root
if let Some(document) = web_sys::window().and_then(|w| w.document()) {
if let Some(root) = document.document_element() {
let _ = root.set_attribute("data-theme", new_theme.value());
}
}
// Update state
current_theme.set(new_theme);
// Update available colors after theme change
available_colors.set(get_theme_event_colors());
})
};
// Apply initial theme on mount
{
let current_theme = current_theme.clone();
use_effect_with((), move |_| {
let theme = (*current_theme).clone();
if let Some(document) = web_sys::window().and_then(|w| w.document()) {
if let Some(root) = document.document_element() {
let _ = root.set_attribute("data-theme", theme.value());
}
}
});
}
// Fetch user info when token is available
{
let user_info = user_info.clone();
@@ -347,7 +413,7 @@ pub fn App() -> Html {
let on_event_update = {
let auth_token = auth_token.clone();
Callback::from(move |(original_event, new_start, new_end): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| {
Callback::from(move |(original_event, new_start, new_end, preserve_rrule, until_date): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)| {
web_sys::console::log_1(&format!("Updating event: {} to new times: {} - {}",
original_event.uid,
new_start.format("%Y-%m-%d %H:%M"),
@@ -428,7 +494,10 @@ pub fn App() -> Html {
reminder_str,
recurrence_str,
recurrence_days,
original_event.calendar_path
original_event.calendar_path,
original_event.exception_dates.clone(),
if preserve_rrule { Some("update_series".to_string()) } else { None },
until_date
).await {
Ok(_) => {
web_sys::console::log_1(&"Event updated successfully".into());
@@ -506,10 +575,12 @@ pub fn App() -> Html {
color_picker_open={(*color_picker_open).clone()}
on_color_change={on_color_change}
on_color_picker_toggle={on_color_picker_toggle}
available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()}
available_colors={(*available_colors).clone()}
on_calendar_context_menu={on_calendar_context_menu}
current_view={(*current_view).clone()}
on_view_change={on_view_change}
current_theme={(*current_theme).clone()}
on_theme_change={on_theme_change}
/>
<main class="app-main">
<RouteHandler
@@ -919,7 +990,10 @@ pub fn App() -> Html {
reminder_str,
recurrence_str,
updated_data.recurrence_days,
updated_data.selected_calendar
updated_data.selected_calendar,
original_event.exception_dates.clone(),
Some("update_series".to_string()), // This is for event edit modal, preserve original RRULE
None // No until_date for edit modal
).await {
Ok(_) => {
web_sys::console::log_1(&"Event updated successfully".into());

View File

@@ -24,7 +24,7 @@ pub struct CalendarProps {
#[prop_or_default]
pub on_create_event_request: Option<Callback<EventCreationData>>,
#[prop_or_default]
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)>>,
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>,
#[prop_or_default]
pub context_menus_open: bool,
}
@@ -194,9 +194,9 @@ pub fn Calendar(props: &CalendarProps) -> Html {
// 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): (CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime)| {
Callback::from(move |(event, new_start, new_end, preserve_rrule, until_date): (CalendarEvent, 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));
callback.emit((event, new_start, new_end, preserve_rrule, until_date));
}
})
};

View File

@@ -25,7 +25,7 @@ pub use context_menu::ContextMenu;
pub use event_context_menu::{EventContextMenu, DeleteAction};
pub use calendar_context_menu::CalendarContextMenu;
pub use create_event_modal::{CreateEventModal, EventCreationData, EventStatus, EventClass, ReminderType, RecurrenceType};
pub use sidebar::{Sidebar, ViewMode};
pub use sidebar::{Sidebar, ViewMode, Theme};
pub use calendar_list_item::CalendarListItem;
pub use route_handler::RouteHandler;
pub use recurring_edit_modal::{RecurringEditModal, RecurringEditAction};

View File

@@ -27,7 +27,7 @@ pub struct RouteHandlerProps {
#[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)>>,
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>,
#[prop_or_default]
pub context_menus_open: bool,
}
@@ -105,7 +105,7 @@ pub struct CalendarViewProps {
#[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)>>,
pub on_event_update_request: Option<Callback<(CalendarEvent, chrono::NaiveDateTime, chrono::NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>,
#[prop_or_default]
pub context_menus_open: bool,
}

View File

@@ -20,6 +20,59 @@ pub enum ViewMode {
Week,
}
#[derive(Clone, PartialEq)]
pub enum Theme {
Default,
Ocean,
Forest,
Sunset,
Purple,
Dark,
Rose,
Mint,
}
impl Theme {
pub fn name(&self) -> &'static str {
match self {
Theme::Default => "Default",
Theme::Ocean => "Ocean",
Theme::Forest => "Forest",
Theme::Sunset => "Sunset",
Theme::Purple => "Purple",
Theme::Dark => "Dark",
Theme::Rose => "Rose",
Theme::Mint => "Mint",
}
}
pub fn value(&self) -> &'static str {
match self {
Theme::Default => "default",
Theme::Ocean => "ocean",
Theme::Forest => "forest",
Theme::Sunset => "sunset",
Theme::Purple => "purple",
Theme::Dark => "dark",
Theme::Rose => "rose",
Theme::Mint => "mint",
}
}
pub fn from_value(value: &str) -> Self {
match value {
"ocean" => Theme::Ocean,
"forest" => Theme::Forest,
"sunset" => Theme::Sunset,
"purple" => Theme::Purple,
"dark" => Theme::Dark,
"rose" => Theme::Rose,
"mint" => Theme::Mint,
_ => Theme::Default,
}
}
}
impl Default for ViewMode {
fn default() -> Self {
ViewMode::Month
@@ -38,6 +91,8 @@ pub struct SidebarProps {
pub on_calendar_context_menu: Callback<(MouseEvent, String)>,
pub current_view: ViewMode,
pub on_view_change: Callback<ViewMode>,
pub current_theme: Theme,
pub on_theme_change: Callback<Theme>,
}
#[function_component(Sidebar)]
@@ -57,6 +112,18 @@ pub fn sidebar(props: &SidebarProps) -> Html {
})
};
let on_theme_change = {
let on_theme_change = props.on_theme_change.clone();
Callback::from(move |e: Event| {
let target = e.target_dyn_into::<HtmlSelectElement>();
if let Some(select) = target {
let value = select.value();
let new_theme = Theme::from_value(&value);
on_theme_change.emit(new_theme);
}
})
};
html! {
<aside class="app-sidebar">
<div class="sidebar-header">
@@ -120,6 +187,19 @@ pub fn sidebar(props: &SidebarProps) -> Html {
</select>
</div>
<div class="theme-selector">
<select class="theme-selector-dropdown" onchange={on_theme_change}>
<option value="default" selected={matches!(props.current_theme, Theme::Default)}>{"🎨 Default"}</option>
<option value="ocean" selected={matches!(props.current_theme, Theme::Ocean)}>{"🌊 Ocean"}</option>
<option value="forest" selected={matches!(props.current_theme, Theme::Forest)}>{"🌲 Forest"}</option>
<option value="sunset" selected={matches!(props.current_theme, Theme::Sunset)}>{"🌅 Sunset"}</option>
<option value="purple" selected={matches!(props.current_theme, Theme::Purple)}>{"💜 Purple"}</option>
<option value="dark" selected={matches!(props.current_theme, Theme::Dark)}>{"🌙 Dark"}</option>
<option value="rose" selected={matches!(props.current_theme, Theme::Rose)}>{"🌹 Rose"}</option>
<option value="mint" selected={matches!(props.current_theme, Theme::Mint)}>{"🍃 Mint"}</option>
</select>
</div>
<button onclick={props.on_logout.reform(|_| ())} class="logout-button">{"Logout"}</button>
</div>
</aside>

View File

@@ -24,7 +24,7 @@ pub struct WeekViewProps {
#[prop_or_default]
pub on_create_event_request: Option<Callback<EventCreationData>>,
#[prop_or_default]
pub on_event_update: Option<Callback<(CalendarEvent, NaiveDateTime, NaiveDateTime)>>,
pub on_event_update: Option<Callback<(CalendarEvent, NaiveDateTime, NaiveDateTime, bool, Option<chrono::DateTime<chrono::Utc>>)>>,
#[prop_or_default]
pub context_menus_open: bool,
#[prop_or_default]
@@ -106,6 +106,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let on_event_update = props.on_event_update.clone();
let on_create_event = props.on_create_event.clone();
let on_create_event_request = props.on_create_event_request.clone();
let events = props.events.clone();
Callback::from(move |action: RecurringEditAction| {
if let Some(edit) = (*pending_recurring_edit).clone() {
match action {
@@ -127,60 +128,142 @@ pub fn week_view(props: &WeekViewProps) -> Html {
).into());
// Update the original series with the exception (times unchanged)
update_callback.emit((updated_series, original_start, original_end));
update_callback.emit((updated_series, original_start, original_end, true, None)); // preserve_rrule = true for EXDATE, no until_date
}
// 2. Then create the new single event using the update callback (to avoid refresh)
if let Some(update_callback) = &on_event_update {
let mut new_event = edit.event.clone();
new_event.uid = format!("{}-exception-{}", edit.event.uid, edit.event.start.timestamp());
new_event.start = chrono::DateTime::from_naive_utc_and_offset(edit.new_start, chrono::Utc);
new_event.end = Some(chrono::DateTime::from_naive_utc_and_offset(edit.new_end, chrono::Utc));
new_event.recurrence_rule = None; // This is now a single event
new_event.exception_dates.clear(); // No exception dates for single event
// 2. Then create the new single event using the create callback
if let Some(create_callback) = &on_create_event_request {
// Convert to EventCreationData for single event
let event_data = EventCreationData {
title: edit.event.summary.clone().unwrap_or_default(),
description: edit.event.description.clone().unwrap_or_default(),
start_date: edit.new_start.date(),
start_time: edit.new_start.time(),
end_date: edit.new_end.date(),
end_time: edit.new_end.time(),
location: edit.event.location.clone().unwrap_or_default(),
all_day: edit.event.all_day,
status: EventStatus::Confirmed,
class: EventClass::Public,
priority: edit.event.priority,
organizer: edit.event.organizer.clone().unwrap_or_default(),
attendees: edit.event.attendees.join(","),
categories: edit.event.categories.join(","),
reminder: ReminderType::None,
recurrence: RecurrenceType::None, // Single event, no recurrence
recurrence_days: vec![false; 7],
selected_calendar: edit.event.calendar_path.clone(),
};
// Use update callback to create the new event (should work without refresh)
update_callback.emit((new_event, edit.new_start, edit.new_end));
// Create the single event
create_callback.emit(event_data);
}
},
RecurringEditAction::FutureEvents => {
// Split series and modify future events
web_sys::console::log_1(&format!("🔄 Splitting series and modifying future: {}",
edit.event.summary.as_deref().unwrap_or("Untitled")).into());
// 1. Update original series to end before this occurrence
// 1. Update original series to set UNTIL to end before this occurrence
if let Some(update_callback) = &on_event_update {
let mut original_series = edit.event.clone();
// Add UNTIL clause to end the series before this occurrence
let until_date = edit.event.start - chrono::Duration::days(1);
// Parse existing RRULE and add UNTIL
if let Some(rrule) = &original_series.recurrence_rule {
let updated_rrule = if rrule.contains("UNTIL=") {
// Replace existing UNTIL
let re = regex::Regex::new(r"UNTIL=[^;]*").unwrap();
re.replace(rrule, &format!("UNTIL={}", until_date.format("%Y%m%dT%H%M%SZ"))).to_string()
// Find the original series event (not the occurrence)
// UIDs like "uuid-timestamp" need to split on the last hyphen, not the first
let base_uid = if let Some(last_hyphen_pos) = edit.event.uid.rfind('-') {
let suffix = &edit.event.uid[last_hyphen_pos + 1..];
// Check if suffix is numeric (timestamp), if so remove it
if suffix.chars().all(|c| c.is_numeric()) {
edit.event.uid[..last_hyphen_pos].to_string()
} else {
// Add UNTIL to existing rule
format!("{};UNTIL={}", rrule, until_date.format("%Y%m%dT%H%M%SZ"))
};
original_series.recurrence_rule = Some(updated_rrule);
edit.event.uid.clone()
}
} else {
edit.event.uid.clone()
};
web_sys::console::log_1(&format!("🔍 Looking for original series: '{}' from occurrence: '{}'", base_uid, edit.event.uid).into());
// Find the original series event by searching for the base UID
let mut original_series = None;
for events_list in events.values() {
for event in events_list {
if event.uid == base_uid {
original_series = Some(event.clone());
break;
}
}
if original_series.is_some() {
break;
}
}
// Update original series
update_callback.emit((original_series, edit.event.start.with_timezone(&chrono::Local).naive_local(), edit.event.end.unwrap_or(edit.event.start).with_timezone(&chrono::Local).naive_local()));
let mut original_series = match original_series {
Some(series) => {
web_sys::console::log_1(&format!("✅ Found original series: '{}'", series.uid).into());
series
},
None => {
web_sys::console::log_1(&format!("⚠️ Could not find original series '{}', using occurrence but fixing UID", base_uid).into());
let mut fallback_event = edit.event.clone();
// Ensure the UID is the base UID, not the occurrence UID
fallback_event.uid = base_uid.clone();
fallback_event
}
};
// Calculate the day before this occurrence for UNTIL clause
let until_date = edit.event.start.date_naive() - chrono::Duration::days(1);
let until_datetime = until_date.and_time(chrono::NaiveTime::from_hms_opt(23, 59, 59).unwrap());
let until_utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(until_datetime, chrono::Utc);
web_sys::console::log_1(&format!("🔄 Will set UNTIL {} for original series to end before occurrence {}",
until_utc.format("%Y-%m-%d %H:%M:%S UTC"),
edit.event.start.format("%Y-%m-%d %H:%M:%S UTC")).into());
// Use the original series start time (not the dragged occurrence time)
let original_start = original_series.start.with_timezone(&chrono::Local).naive_local();
let original_end = original_series.end.unwrap_or(original_series.start).with_timezone(&chrono::Local).naive_local();
// Send until_date to backend instead of modifying RRULE on frontend
update_callback.emit((original_series, original_start, original_end, true, Some(until_utc))); // preserve_rrule = true, backend will add UNTIL
}
// 2. Create new series starting from this occurrence with modified times
if let Some(update_callback) = &on_event_update {
let mut new_series = edit.event.clone();
new_series.uid = format!("{}-future-{}", edit.event.uid, edit.event.start.timestamp());
new_series.start = chrono::DateTime::from_naive_utc_and_offset(edit.new_start, chrono::Utc);
new_series.end = Some(chrono::DateTime::from_naive_utc_and_offset(edit.new_end, chrono::Utc));
new_series.exception_dates.clear(); // New series has no exceptions
if let Some(create_callback) = &on_create_event_request {
// Convert the recurring event to EventCreationData for the create callback
let event_data = EventCreationData {
title: edit.event.summary.clone().unwrap_or_default(),
description: edit.event.description.clone().unwrap_or_default(),
start_date: edit.new_start.date(),
start_time: edit.new_start.time(),
end_date: edit.new_end.date(),
end_time: edit.new_end.time(),
location: edit.event.location.clone().unwrap_or_default(),
all_day: edit.event.all_day,
status: EventStatus::Confirmed, // Default status
class: EventClass::Public, // Default class
priority: edit.event.priority,
organizer: edit.event.organizer.clone().unwrap_or_default(),
attendees: edit.event.attendees.join(","),
categories: edit.event.categories.join(","),
reminder: ReminderType::None, // Default reminder
recurrence: if let Some(rrule) = &edit.event.recurrence_rule {
if rrule.contains("FREQ=DAILY") {
RecurrenceType::Daily
} else if rrule.contains("FREQ=WEEKLY") {
RecurrenceType::Weekly
} else if rrule.contains("FREQ=MONTHLY") {
RecurrenceType::Monthly
} else if rrule.contains("FREQ=YEARLY") {
RecurrenceType::Yearly
} else {
RecurrenceType::None
}
} else {
RecurrenceType::None
},
recurrence_days: vec![false; 7], // Default days
selected_calendar: edit.event.calendar_path.clone(),
};
// Use update callback to create the new series (should work without refresh)
update_callback.emit((new_series, edit.new_start, edit.new_end));
// Create the new series
create_callback.emit(event_data);
}
},
RecurringEditAction::AllEvents => {
@@ -188,7 +271,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
let series_event = edit.event.clone();
if let Some(callback) = &on_event_update {
callback.emit((series_event, edit.new_start, edit.new_end));
callback.emit((series_event, edit.new_start, edit.new_end, true, None)); // Regular drag operation - preserve RRULE, no until_date
}
},
}
@@ -382,7 +465,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
} else {
// Regular event - proceed with update
if let Some(callback) = &on_event_update {
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date
}
}
},
@@ -418,7 +501,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
} else {
// Regular event - proceed with update
if let Some(callback) = &on_event_update {
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date
}
}
},
@@ -449,7 +532,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
} else {
// Regular event - proceed with update
if let Some(callback) = &on_event_update {
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
callback.emit((event.clone(), new_start_datetime, new_end_datetime, true, None)); // Regular drag operation - preserve RRULE, no until_date
}
}
}

View File

@@ -718,7 +718,7 @@ impl CalendarService {
let body_string = serde_json::to_string(&body)
.map_err(|e| format!("JSON serialization failed: {}", e))?;
let url = format!("{}/calendar/events/create", self.base_url);
opts.set_body(&body_string.into());
let request = Request::new_with_str_and_init(&url, &opts)
@@ -777,7 +777,10 @@ impl CalendarService {
reminder: String,
recurrence: String,
recurrence_days: Vec<bool>,
calendar_path: Option<String>
calendar_path: Option<String>,
exception_dates: Vec<DateTime<Utc>>,
update_action: Option<String>,
until_date: Option<DateTime<Utc>>
) -> Result<(), String> {
let window = web_sys::window().ok_or("No global window exists")?;
@@ -805,13 +808,15 @@ impl CalendarService {
"recurrence": recurrence,
"recurrence_days": recurrence_days,
"calendar_path": calendar_path,
"update_action": "update_series",
"occurrence_date": null
"update_action": update_action,
"occurrence_date": null,
"exception_dates": exception_dates.iter().map(|dt| dt.to_rfc3339()).collect::<Vec<String>>(),
"until_date": until_date.as_ref().map(|dt| dt.to_rfc3339())
});
let body_string = serde_json::to_string(&body)
.map_err(|e| format!("JSON serialization failed: {}", e))?;
let url = format!("{}/calendar/events/update", self.base_url);
opts.set_body(&body_string.into());
let request = Request::new_with_str_and_init(&url, &opts)