- Replace separate Create Calendar and External Calendar modals with unified tabbed interface - Redesign modal styling with less rounded corners and cleaner appearance - Significantly increase padding throughout modal components for better spacing - Fix CSS variable self-references (control-padding, standard-transition) - Improve button styling with better padding (0.875rem 2rem) and colors - Enhance form elements with generous padding (1rem) and improved focus states - Redesign tab bar with segmented control appearance and proper active states - Update context menus with modern glassmorphism styling and smooth animations - Consolidate calendar management functionality into single reusable component 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1667 lines
86 KiB
Rust
1667 lines
86 KiB
Rust
use crate::components::{
|
||
CalendarContextMenu, CalendarManagementModal, ContextMenu, CreateEventModal, DeleteAction,
|
||
EditAction, EventContextMenu, EventModal, EventCreationData,
|
||
MobileWarningModal, RouteHandler, Sidebar, Theme, ViewMode,
|
||
};
|
||
use crate::components::mobile_warning_modal::is_mobile_device;
|
||
use crate::components::sidebar::{Style};
|
||
use crate::models::ical::VEvent;
|
||
use crate::services::{calendar_service::{UserInfo, ExternalCalendar}, CalendarService};
|
||
use chrono::NaiveDate;
|
||
use gloo_storage::{LocalStorage, Storage};
|
||
use gloo_timers::callback::Interval;
|
||
use wasm_bindgen::JsCast;
|
||
use web_sys::MouseEvent;
|
||
use yew::prelude::*;
|
||
use yew_router::prelude::*;
|
||
|
||
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 {
|
||
let auth_token = use_state(|| -> Option<String> { None });
|
||
|
||
// Validate token on app startup
|
||
{
|
||
let auth_token = auth_token.clone();
|
||
use_effect_with((), move |_| {
|
||
let auth_token = auth_token.clone();
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
// Check if there's a stored token
|
||
if let Ok(stored_token) = LocalStorage::get::<String>("auth_token") {
|
||
// Verify the stored token
|
||
let auth_service = crate::auth::AuthService::new();
|
||
match auth_service.verify_token(&stored_token).await {
|
||
Ok(true) => {
|
||
// Token is valid, set it
|
||
web_sys::console::log_1(&"✅ Stored auth token is valid".into());
|
||
auth_token.set(Some(stored_token));
|
||
}
|
||
_ => {
|
||
// Token is invalid or verification failed, clear it
|
||
web_sys::console::log_1(&"❌ Stored auth token is invalid, clearing".into());
|
||
let _ = LocalStorage::delete("auth_token");
|
||
let _ = LocalStorage::delete("session_token");
|
||
let _ = LocalStorage::delete("caldav_credentials");
|
||
auth_token.set(None);
|
||
}
|
||
}
|
||
} else {
|
||
// No stored token
|
||
web_sys::console::log_1(&"ℹ️ No stored auth token found".into());
|
||
auth_token.set(None);
|
||
}
|
||
});
|
||
|| ()
|
||
});
|
||
}
|
||
|
||
let user_info = use_state(|| -> Option<UserInfo> { None });
|
||
let color_picker_open = use_state(|| -> Option<String> { None });
|
||
let calendar_management_modal_open = use_state(|| false);
|
||
let context_menu_open = use_state(|| false);
|
||
let context_menu_pos = use_state(|| (0i32, 0i32));
|
||
let context_menu_calendar_path = use_state(|| -> Option<String> { None });
|
||
let event_context_menu_open = use_state(|| false);
|
||
let event_context_menu_pos = use_state(|| (0i32, 0i32));
|
||
let event_context_menu_event = use_state(|| -> Option<VEvent> { None });
|
||
let calendar_context_menu_open = use_state(|| false);
|
||
let calendar_context_menu_pos = use_state(|| (0i32, 0i32));
|
||
let calendar_context_menu_date = use_state(|| -> Option<NaiveDate> { None });
|
||
let create_event_modal_open = use_state(|| false);
|
||
let selected_date_for_event = use_state(|| -> Option<NaiveDate> { None });
|
||
let event_edit_scope = use_state(|| -> Option<EditAction> { None });
|
||
let view_event_modal_open = use_state(|| false);
|
||
let view_event_modal_event = use_state(|| -> Option<VEvent> { None });
|
||
let refreshing_calendar_id = use_state(|| -> Option<i32> { None });
|
||
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() });
|
||
|
||
// Mobile warning state
|
||
let mobile_warning_open = use_state(|| is_mobile_device());
|
||
let refresh_interval = use_state(|| -> Option<Interval> { None });
|
||
|
||
// Calendar view state - load from localStorage if available
|
||
let current_view = use_state(|| {
|
||
// Try to load saved view mode from localStorage
|
||
if let Ok(saved_view) = LocalStorage::get::<String>("calendar_view_mode") {
|
||
match saved_view.as_str() {
|
||
"week" => ViewMode::Week,
|
||
_ => ViewMode::Month,
|
||
}
|
||
} else {
|
||
ViewMode::Month // Default to month view
|
||
}
|
||
});
|
||
|
||
// 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
|
||
}
|
||
});
|
||
|
||
// Style state - load from localStorage if available
|
||
let current_style = use_state(|| {
|
||
// Try to load saved style from localStorage
|
||
if let Ok(saved_style) = LocalStorage::get::<String>("calendar_style") {
|
||
Style::from_value(&saved_style)
|
||
} else {
|
||
Style::Default // Default style
|
||
}
|
||
});
|
||
|
||
let available_colors = use_state(|| get_theme_event_colors());
|
||
|
||
// Function to refresh calendar data without full page reload
|
||
let refresh_calendar_data = {
|
||
let user_info = user_info.clone();
|
||
let auth_token = auth_token.clone();
|
||
let external_calendars = external_calendars.clone();
|
||
let external_calendar_events = external_calendar_events.clone();
|
||
|
||
Callback::from(move |_| {
|
||
let user_info = user_info.clone();
|
||
let auth_token = auth_token.clone();
|
||
let external_calendars = external_calendars.clone();
|
||
let external_calendar_events = external_calendar_events.clone();
|
||
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
// Refresh main calendar data if authenticated
|
||
if let Some(token) = (*auth_token).clone() {
|
||
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()
|
||
};
|
||
|
||
if !password.is_empty() {
|
||
match calendar_service.fetch_user_info(&token, &password).await {
|
||
Ok(mut info) => {
|
||
// Apply saved colors
|
||
if let Ok(saved_colors_json) =
|
||
LocalStorage::get::<String>("calendar_colors")
|
||
{
|
||
if let Ok(saved_info) =
|
||
serde_json::from_str::<UserInfo>(&saved_colors_json)
|
||
{
|
||
for saved_cal in &saved_info.calendars {
|
||
for cal in &mut info.calendars {
|
||
if cal.path == saved_cal.path {
|
||
cal.color = saved_cal.color.clone();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// Add timestamp to force re-render
|
||
info.last_updated = (js_sys::Date::now() / 1000.0) as u64;
|
||
user_info.set(Some(info));
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::log_1(
|
||
&format!("Failed to refresh main calendar data: {}", err).into(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Refresh external calendars data
|
||
match CalendarService::get_external_calendars().await {
|
||
Ok(calendars) => {
|
||
external_calendars.set(calendars.clone());
|
||
|
||
// Load events for visible external calendars
|
||
let mut all_external_events = Vec::new();
|
||
for calendar in calendars {
|
||
if calendar.is_visible {
|
||
match CalendarService::fetch_external_calendar_events(calendar.id).await {
|
||
Ok(mut events) => {
|
||
// Set calendar_path for color matching
|
||
for event in &mut events {
|
||
event.calendar_path = Some(format!("external_{}", calendar.id));
|
||
}
|
||
all_external_events.extend(events);
|
||
}
|
||
Err(e) => {
|
||
web_sys::console::log_1(
|
||
&format!("Failed to fetch events for external calendar {}: {}", calendar.id, e).into(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
external_calendar_events.set(all_external_events);
|
||
}
|
||
Err(e) => {
|
||
web_sys::console::log_1(
|
||
&format!("Failed to refresh external calendars: {}", e).into(),
|
||
);
|
||
}
|
||
}
|
||
|
||
});
|
||
})
|
||
};
|
||
|
||
let on_login = {
|
||
let auth_token = auth_token.clone();
|
||
Callback::from(move |token: String| {
|
||
auth_token.set(Some(token));
|
||
})
|
||
};
|
||
|
||
let on_logout = {
|
||
let auth_token = auth_token.clone();
|
||
let user_info = user_info.clone();
|
||
Callback::from(move |_| {
|
||
let _ = LocalStorage::delete("auth_token");
|
||
auth_token.set(None);
|
||
user_info.set(None);
|
||
})
|
||
};
|
||
|
||
let on_mobile_warning_close = {
|
||
let mobile_warning_open = mobile_warning_open.clone();
|
||
Callback::from(move |_| {
|
||
mobile_warning_open.set(false);
|
||
})
|
||
};
|
||
|
||
let on_view_change = {
|
||
let current_view = current_view.clone();
|
||
Callback::from(move |new_view: ViewMode| {
|
||
// Save view mode to localStorage
|
||
let view_string = match new_view {
|
||
ViewMode::Month => "month",
|
||
ViewMode::Week => "week",
|
||
};
|
||
let _ = LocalStorage::set("calendar_view_mode", view_string);
|
||
|
||
// Update state
|
||
current_view.set(new_view);
|
||
})
|
||
};
|
||
|
||
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());
|
||
})
|
||
};
|
||
|
||
let on_style_change = {
|
||
let current_style = current_style.clone();
|
||
Callback::from(move |new_style: Style| {
|
||
// Save style to localStorage
|
||
let _ = LocalStorage::set("calendar_style", new_style.value());
|
||
|
||
// Hot-swap stylesheet
|
||
if let Some(window) = web_sys::window() {
|
||
if let Some(document) = window.document() {
|
||
// Remove existing style link if it exists
|
||
if let Some(existing_link) = document.get_element_by_id("dynamic-style") {
|
||
existing_link.remove();
|
||
}
|
||
|
||
// Create and append new stylesheet link only if style has a path
|
||
if let Some(stylesheet_path) = new_style.stylesheet_path() {
|
||
if let Ok(link) = document.create_element("link") {
|
||
let link = link.dyn_into::<web_sys::HtmlLinkElement>().unwrap();
|
||
link.set_id("dynamic-style");
|
||
link.set_rel("stylesheet");
|
||
link.set_href(stylesheet_path);
|
||
|
||
if let Some(head) = document.head() {
|
||
let _ = head.append_child(&link);
|
||
}
|
||
}
|
||
}
|
||
// If stylesheet_path is None (Default style), just removing the dynamic link is enough
|
||
}
|
||
}
|
||
|
||
// Update state
|
||
current_style.set(new_style);
|
||
})
|
||
};
|
||
|
||
// 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());
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Apply initial style on mount
|
||
{
|
||
let current_style = current_style.clone();
|
||
use_effect_with((), move |_| {
|
||
let style = (*current_style).clone();
|
||
if let Some(window) = web_sys::window() {
|
||
if let Some(document) = window.document() {
|
||
// Create and append stylesheet link for initial style only if it has a path
|
||
if let Some(stylesheet_path) = style.stylesheet_path() {
|
||
if let Ok(link) = document.create_element("link") {
|
||
let link = link.dyn_into::<web_sys::HtmlLinkElement>().unwrap();
|
||
link.set_id("dynamic-style");
|
||
link.set_rel("stylesheet");
|
||
link.set_href(stylesheet_path);
|
||
|
||
if let Some(head) = document.head() {
|
||
let _ = head.append_child(&link);
|
||
}
|
||
}
|
||
}
|
||
// If initial style is Default (None), no additional stylesheet needed
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Fetch user info when token is available
|
||
{
|
||
let user_info = user_info.clone();
|
||
let auth_token = auth_token.clone();
|
||
|
||
use_effect_with((*auth_token).clone(), move |token| {
|
||
if let Some(token) = token {
|
||
let user_info = user_info.clone();
|
||
let token = token.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()
|
||
};
|
||
|
||
if !password.is_empty() {
|
||
match calendar_service.fetch_user_info(&token, &password).await {
|
||
Ok(mut info) => {
|
||
if let Ok(saved_colors_json) =
|
||
LocalStorage::get::<String>("calendar_colors")
|
||
{
|
||
if let Ok(saved_info) =
|
||
serde_json::from_str::<UserInfo>(&saved_colors_json)
|
||
{
|
||
for saved_cal in &saved_info.calendars {
|
||
for cal in &mut info.calendars {
|
||
if cal.path == saved_cal.path {
|
||
cal.color = saved_cal.color.clone();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
user_info.set(Some(info));
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::log_1(
|
||
&format!("Failed to fetch user info: {}", err).into(),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
user_info.set(None);
|
||
}
|
||
|
||
|| ()
|
||
});
|
||
}
|
||
|
||
// Function to refresh external calendars
|
||
let refresh_external_calendars = {
|
||
let external_calendars = external_calendars.clone();
|
||
let external_calendar_events = external_calendar_events.clone();
|
||
Callback::from(move |_| {
|
||
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(mut events) = CalendarService::fetch_external_calendar_events(calendar.id).await {
|
||
// Set calendar_path for color matching
|
||
for event in &mut events {
|
||
event.calendar_path = Some(format!("external_{}", calendar.id));
|
||
}
|
||
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(),
|
||
);
|
||
}
|
||
}
|
||
});
|
||
})
|
||
};
|
||
|
||
// Load external calendars when auth token is available and set up auto-refresh
|
||
{
|
||
let auth_token = auth_token.clone();
|
||
let refresh_external_calendars = refresh_external_calendars.clone();
|
||
let refresh_interval = refresh_interval.clone();
|
||
let external_calendars = external_calendars.clone();
|
||
let external_calendar_events = external_calendar_events.clone();
|
||
|
||
use_effect_with((*auth_token).clone(), move |token| {
|
||
if let Some(_) = token {
|
||
// Initial load
|
||
refresh_external_calendars.emit(());
|
||
|
||
// Set up 5-minute refresh interval
|
||
let refresh_external_calendars = refresh_external_calendars.clone();
|
||
let interval = Interval::new(5 * 60 * 1000, move || {
|
||
refresh_external_calendars.emit(());
|
||
});
|
||
refresh_interval.set(Some(interval));
|
||
} else {
|
||
// Clear data and interval when logged out
|
||
external_calendars.set(Vec::new());
|
||
external_calendar_events.set(Vec::new());
|
||
refresh_interval.set(None);
|
||
}
|
||
|
||
// Cleanup function
|
||
let refresh_interval = refresh_interval.clone();
|
||
move || {
|
||
// Clear interval on cleanup
|
||
refresh_interval.set(None);
|
||
}
|
||
});
|
||
}
|
||
|
||
let on_outside_click = {
|
||
let color_picker_open = color_picker_open.clone();
|
||
let context_menu_open = context_menu_open.clone();
|
||
let event_context_menu_open = event_context_menu_open.clone();
|
||
let calendar_context_menu_open = calendar_context_menu_open.clone();
|
||
Callback::from(move |e: MouseEvent| {
|
||
// Check if any context menu or color picker is open
|
||
let any_menu_open = color_picker_open.is_some()
|
||
|| *context_menu_open
|
||
|| *event_context_menu_open
|
||
|| *calendar_context_menu_open;
|
||
|
||
if any_menu_open {
|
||
// Prevent the default action and stop event propagation
|
||
e.prevent_default();
|
||
e.stop_propagation();
|
||
}
|
||
|
||
// Close all open menus/pickers
|
||
color_picker_open.set(None);
|
||
context_menu_open.set(false);
|
||
event_context_menu_open.set(false);
|
||
calendar_context_menu_open.set(false);
|
||
})
|
||
};
|
||
|
||
// Compute if any context menu is open
|
||
let any_context_menu_open = color_picker_open.is_some()
|
||
|| *context_menu_open
|
||
|| *event_context_menu_open
|
||
|| *calendar_context_menu_open;
|
||
|
||
let on_color_change = {
|
||
let user_info = user_info.clone();
|
||
let color_picker_open = color_picker_open.clone();
|
||
Callback::from(move |(calendar_path, color): (String, String)| {
|
||
if let Some(mut info) = (*user_info).clone() {
|
||
for calendar in &mut info.calendars {
|
||
if calendar.path == calendar_path {
|
||
calendar.color = color.clone();
|
||
break;
|
||
}
|
||
}
|
||
user_info.set(Some(info.clone()));
|
||
|
||
if let Ok(json) = serde_json::to_string(&info) {
|
||
let _ = LocalStorage::set("calendar_colors", json);
|
||
}
|
||
}
|
||
color_picker_open.set(None);
|
||
})
|
||
};
|
||
|
||
let on_color_picker_toggle = {
|
||
let color_picker_open = color_picker_open.clone();
|
||
Callback::from(move |calendar_path: String| {
|
||
if color_picker_open.as_ref() == Some(&calendar_path) {
|
||
color_picker_open.set(None);
|
||
} else {
|
||
color_picker_open.set(Some(calendar_path));
|
||
}
|
||
})
|
||
};
|
||
|
||
let on_calendar_context_menu = {
|
||
let context_menu_open = context_menu_open.clone();
|
||
let context_menu_pos = context_menu_pos.clone();
|
||
let context_menu_calendar_path = context_menu_calendar_path.clone();
|
||
Callback::from(move |(event, calendar_path): (MouseEvent, String)| {
|
||
context_menu_open.set(true);
|
||
context_menu_pos.set((event.client_x(), event.client_y()));
|
||
context_menu_calendar_path.set(Some(calendar_path));
|
||
})
|
||
};
|
||
|
||
let on_event_context_menu = {
|
||
let event_context_menu_open = event_context_menu_open.clone();
|
||
let event_context_menu_pos = event_context_menu_pos.clone();
|
||
let event_context_menu_event = event_context_menu_event.clone();
|
||
Callback::from(move |(event, calendar_event): (MouseEvent, VEvent)| {
|
||
event_context_menu_open.set(true);
|
||
event_context_menu_pos.set((event.client_x(), event.client_y()));
|
||
event_context_menu_event.set(Some(calendar_event));
|
||
})
|
||
};
|
||
|
||
let on_calendar_date_context_menu = {
|
||
let calendar_context_menu_open = calendar_context_menu_open.clone();
|
||
let calendar_context_menu_pos = calendar_context_menu_pos.clone();
|
||
let calendar_context_menu_date = calendar_context_menu_date.clone();
|
||
Callback::from(move |(event, date): (MouseEvent, NaiveDate)| {
|
||
calendar_context_menu_open.set(true);
|
||
calendar_context_menu_pos.set((event.client_x(), event.client_y()));
|
||
calendar_context_menu_date.set(Some(date));
|
||
})
|
||
};
|
||
|
||
let on_create_event_click = {
|
||
let create_event_modal_open = create_event_modal_open.clone();
|
||
let selected_date_for_event = selected_date_for_event.clone();
|
||
let calendar_context_menu_date = calendar_context_menu_date.clone();
|
||
Callback::from(move |_: MouseEvent| {
|
||
create_event_modal_open.set(true);
|
||
selected_date_for_event.set((*calendar_context_menu_date).clone());
|
||
})
|
||
};
|
||
|
||
let on_event_create = {
|
||
let create_event_modal_open = create_event_modal_open.clone();
|
||
let auth_token = auth_token.clone();
|
||
let refresh_calendar_data = refresh_calendar_data.clone();
|
||
Callback::from(move |event_data: EventCreationData| {
|
||
// Check if this is an update operation (has original_uid) or a create operation
|
||
if let Some(original_uid) = event_data.original_uid.clone() {
|
||
web_sys::console::log_1(&format!("Updating event via modal: {:?}", event_data).into());
|
||
|
||
create_event_modal_open.set(false);
|
||
|
||
// Handle the update operation using the existing backend update logic
|
||
if let Some(token) = (*auth_token).clone() {
|
||
let event_data_for_update = event_data.clone();
|
||
let refresh_callback = refresh_calendar_data.clone();
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
let calendar_service = CalendarService::new();
|
||
|
||
// Get CalDAV password from storage
|
||
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()
|
||
};
|
||
|
||
// Convert EventCreationData to update parameters
|
||
let params = event_data_for_update.to_create_event_params();
|
||
|
||
// Determine if this is a recurring event update
|
||
let is_recurring = matches!(event_data_for_update.recurrence, crate::components::event_form::RecurrenceType::Daily |
|
||
crate::components::event_form::RecurrenceType::Weekly |
|
||
crate::components::event_form::RecurrenceType::Monthly |
|
||
crate::components::event_form::RecurrenceType::Yearly);
|
||
|
||
let update_result = if is_recurring && event_data_for_update.edit_scope.is_some() {
|
||
// Use series update endpoint for recurring events
|
||
let edit_action = event_data_for_update.edit_scope.unwrap();
|
||
let scope = match edit_action {
|
||
crate::components::EditAction::EditAll => "all_in_series".to_string(),
|
||
crate::components::EditAction::EditFuture => "this_and_future".to_string(),
|
||
crate::components::EditAction::EditThis => "this_only".to_string(),
|
||
};
|
||
|
||
calendar_service
|
||
.update_series(
|
||
&token,
|
||
&password,
|
||
original_uid.clone(),
|
||
params.0, // title
|
||
params.1, // description
|
||
params.2, // start_date
|
||
params.3, // start_time
|
||
params.4, // end_date
|
||
params.5, // end_time
|
||
params.6, // location
|
||
params.7, // all_day
|
||
params.8, // status
|
||
params.9, // class
|
||
params.10, // priority
|
||
params.11, // organizer
|
||
params.12, // attendees
|
||
params.13, // categories
|
||
params.14, // reminder
|
||
params.15, // recurrence
|
||
params.16, // recurrence_days
|
||
params.18, // recurrence_count
|
||
params.19, // recurrence_until
|
||
params.17, // calendar_path
|
||
scope,
|
||
event_data_for_update.occurrence_date.map(|d| d.format("%Y-%m-%d").to_string()), // occurrence_date
|
||
)
|
||
.await
|
||
} else {
|
||
// Use regular update endpoint for single events
|
||
calendar_service
|
||
.update_event(
|
||
&token,
|
||
&password,
|
||
original_uid.clone(),
|
||
params.0, // title
|
||
params.1, // description
|
||
params.2, // start_date
|
||
params.3, // start_time
|
||
params.4, // end_date
|
||
params.5, // end_time
|
||
params.6, // location
|
||
params.7, // all_day
|
||
params.8, // status
|
||
params.9, // class
|
||
params.10, // priority
|
||
params.11, // organizer
|
||
params.12, // attendees
|
||
params.13, // categories
|
||
params.14, // reminder
|
||
params.15, // recurrence
|
||
params.16, // recurrence_days
|
||
params.17, // calendar_path
|
||
vec![], // exception_dates - empty for simple updates
|
||
None, // update_action - None for regular updates
|
||
None, // until_date - None for regular updates
|
||
)
|
||
.await
|
||
};
|
||
|
||
match update_result {
|
||
Ok(_) => {
|
||
web_sys::console::log_1(&"Event updated successfully via modal".into());
|
||
// Refresh calendar data without page reload
|
||
refresh_callback.emit(());
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::error_1(
|
||
&format!("Failed to update event: {}", err).into(),
|
||
);
|
||
web_sys::window()
|
||
.unwrap()
|
||
.alert_with_message(&format!("Failed to update event: {}", err))
|
||
.unwrap();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
web_sys::console::log_1(&format!("Creating event: {:?}", event_data).into());
|
||
|
||
// Save the selected calendar as the last used calendar
|
||
if let Some(ref calendar_path) = event_data.selected_calendar {
|
||
let _ = LocalStorage::set("last_used_calendar", calendar_path);
|
||
|
||
// Also sync to backend asynchronously
|
||
let calendar_path_for_sync = calendar_path.clone();
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
let preferences_service = crate::services::preferences::PreferencesService::new();
|
||
if let Err(e) = preferences_service.update_last_used_calendar(&calendar_path_for_sync).await {
|
||
web_sys::console::warn_1(&format!("Failed to sync last used calendar to backend: {}", e).into());
|
||
}
|
||
});
|
||
}
|
||
|
||
create_event_modal_open.set(false);
|
||
|
||
if let Some(_token) = (*auth_token).clone() {
|
||
let refresh_callback = refresh_calendar_data.clone();
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
let _calendar_service = CalendarService::new();
|
||
|
||
// Get CalDAV password from storage
|
||
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 params = event_data.to_create_event_params();
|
||
let create_result = _calendar_service
|
||
.create_event(
|
||
&_token, &_password, params.0, // title
|
||
params.1, // description
|
||
params.2, // start_date
|
||
params.3, // start_time
|
||
params.4, // end_date
|
||
params.5, // end_time
|
||
params.6, // location
|
||
params.7, // all_day
|
||
params.8, // status
|
||
params.9, // class
|
||
params.10, // priority
|
||
params.11, // organizer
|
||
params.12, // attendees
|
||
params.13, // categories
|
||
params.14, // reminder
|
||
params.15, // recurrence
|
||
params.16, // recurrence_days
|
||
params.18, // recurrence_count
|
||
params.19, // recurrence_until
|
||
params.17, // calendar_path
|
||
)
|
||
.await;
|
||
match create_result {
|
||
Ok(_) => {
|
||
web_sys::console::log_1(&"Event created successfully".into());
|
||
// Refresh calendar data without page reload
|
||
refresh_callback.emit(());
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::error_1(
|
||
&format!("Failed to create event: {}", err).into(),
|
||
);
|
||
web_sys::window()
|
||
.unwrap()
|
||
.alert_with_message(&format!("Failed to create event: {}", err))
|
||
.unwrap();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
})
|
||
};
|
||
|
||
let on_event_update = {
|
||
let auth_token = auth_token.clone();
|
||
let refresh_calendar_data = refresh_calendar_data.clone();
|
||
Callback::from(
|
||
move |(
|
||
original_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>,
|
||
)| {
|
||
web_sys::console::log_1(
|
||
&format!(
|
||
"Updating event: {} to new times: {} - {}",
|
||
original_event.uid,
|
||
new_start.format("%Y-%m-%d %H:%M"),
|
||
new_end.format("%Y-%m-%d %H:%M")
|
||
)
|
||
.into(),
|
||
);
|
||
|
||
// Use the original UID for all updates
|
||
let backend_uid = original_event.uid.clone();
|
||
|
||
if let Some(token) = (*auth_token).clone() {
|
||
let original_event = original_event.clone();
|
||
let backend_uid = backend_uid.clone();
|
||
let refresh_callback = refresh_calendar_data.clone();
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
let calendar_service = CalendarService::new();
|
||
|
||
// Get CalDAV password from storage
|
||
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()
|
||
};
|
||
|
||
// Convert local naive datetime to UTC before sending to backend
|
||
use chrono::TimeZone;
|
||
let local_tz = chrono::Local;
|
||
|
||
let start_utc = local_tz.from_local_datetime(&new_start)
|
||
.single()
|
||
.unwrap_or_else(|| {
|
||
// Fallback for ambiguous times (DST transitions)
|
||
local_tz.from_local_datetime(&new_start).earliest().unwrap()
|
||
})
|
||
.with_timezone(&chrono::Utc);
|
||
|
||
let end_utc = local_tz.from_local_datetime(&new_end)
|
||
.single()
|
||
.unwrap_or_else(|| {
|
||
// Fallback for ambiguous times (DST transitions)
|
||
local_tz.from_local_datetime(&new_end).earliest().unwrap()
|
||
})
|
||
.with_timezone(&chrono::Utc);
|
||
|
||
let start_date = start_utc.format("%Y-%m-%d").to_string();
|
||
let start_time = start_utc.format("%H:%M").to_string();
|
||
let end_date = end_utc.format("%Y-%m-%d").to_string();
|
||
let end_time = end_utc.format("%H:%M").to_string();
|
||
|
||
// Convert existing event data to string formats for the API
|
||
let status_str = match original_event.status {
|
||
Some(crate::models::ical::EventStatus::Tentative) => {
|
||
"TENTATIVE".to_string()
|
||
}
|
||
Some(crate::models::ical::EventStatus::Confirmed) => {
|
||
"CONFIRMED".to_string()
|
||
}
|
||
Some(crate::models::ical::EventStatus::Cancelled) => {
|
||
"CANCELLED".to_string()
|
||
}
|
||
None => "CONFIRMED".to_string(), // Default status
|
||
};
|
||
|
||
let class_str = match original_event.class {
|
||
Some(crate::models::ical::EventClass::Public) => "PUBLIC".to_string(),
|
||
Some(crate::models::ical::EventClass::Private) => "PRIVATE".to_string(),
|
||
Some(crate::models::ical::EventClass::Confidential) => {
|
||
"CONFIDENTIAL".to_string()
|
||
}
|
||
None => "PUBLIC".to_string(), // Default class
|
||
};
|
||
|
||
// Convert reminders to string format
|
||
let reminder_str = if !original_event.alarms.is_empty() {
|
||
// Convert from VAlarm to minutes before
|
||
"15".to_string() // TODO: Convert VAlarm trigger to minutes
|
||
} else {
|
||
"".to_string()
|
||
};
|
||
|
||
// Handle recurrence (keep existing)
|
||
let recurrence_str = original_event.rrule.unwrap_or_default();
|
||
let recurrence_days = vec![false; 7]; // Default - could be enhanced to parse existing recurrence
|
||
|
||
// Determine if this is a recurring event that needs series endpoint
|
||
let has_recurrence =
|
||
!recurrence_str.is_empty() && recurrence_str.to_uppercase() != "NONE";
|
||
|
||
let result = if let Some(scope) = update_scope.as_ref() {
|
||
// Use series endpoint for recurring event operations
|
||
if !has_recurrence {
|
||
web_sys::console::log_1(&"⚠️ Warning: update_scope provided for non-recurring event, using regular endpoint instead".into());
|
||
// Fall through to regular endpoint
|
||
None
|
||
} else {
|
||
Some(
|
||
calendar_service
|
||
.update_series(
|
||
&token,
|
||
&password,
|
||
backend_uid.clone(),
|
||
original_event.summary.clone().unwrap_or_default(),
|
||
original_event.description.clone().unwrap_or_default(),
|
||
start_date.clone(),
|
||
start_time.clone(),
|
||
end_date.clone(),
|
||
end_time.clone(),
|
||
original_event.location.clone().unwrap_or_default(),
|
||
original_event.all_day,
|
||
status_str.clone(),
|
||
class_str.clone(),
|
||
original_event.priority,
|
||
original_event
|
||
.organizer
|
||
.as_ref()
|
||
.map(|o| o.cal_address.clone())
|
||
.unwrap_or_default(),
|
||
original_event
|
||
.attendees
|
||
.iter()
|
||
.map(|a| a.cal_address.clone())
|
||
.collect::<Vec<_>>()
|
||
.join(","),
|
||
original_event.categories.join(","),
|
||
reminder_str.clone(),
|
||
recurrence_str.clone(),
|
||
vec![false; 7],
|
||
None,
|
||
None,
|
||
original_event.calendar_path.clone(),
|
||
scope.clone(),
|
||
occurrence_date,
|
||
)
|
||
.await,
|
||
)
|
||
}
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let result = if let Some(series_result) = result {
|
||
series_result
|
||
} else {
|
||
// Use regular endpoint
|
||
calendar_service
|
||
.update_event(
|
||
&token,
|
||
&password,
|
||
backend_uid,
|
||
original_event.summary.unwrap_or_default(),
|
||
original_event.description.unwrap_or_default(),
|
||
start_date,
|
||
start_time,
|
||
end_date,
|
||
end_time,
|
||
original_event.location.unwrap_or_default(),
|
||
original_event.all_day,
|
||
status_str,
|
||
class_str,
|
||
original_event.priority,
|
||
original_event
|
||
.organizer
|
||
.as_ref()
|
||
.map(|o| o.cal_address.clone())
|
||
.unwrap_or_default(),
|
||
original_event
|
||
.attendees
|
||
.iter()
|
||
.map(|a| a.cal_address.clone())
|
||
.collect::<Vec<_>>()
|
||
.join(","),
|
||
original_event.categories.join(","),
|
||
reminder_str,
|
||
recurrence_str,
|
||
recurrence_days,
|
||
original_event.calendar_path,
|
||
original_event.exdate.clone(),
|
||
if preserve_rrule {
|
||
Some("update_series".to_string())
|
||
} else {
|
||
Some("this_and_future".to_string())
|
||
},
|
||
until_date,
|
||
)
|
||
.await
|
||
};
|
||
|
||
match result {
|
||
Ok(_) => {
|
||
web_sys::console::log_1(&"Event updated successfully".into());
|
||
// Refresh calendar data without page reload
|
||
refresh_callback.emit(());
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::error_1(
|
||
&format!("Failed to update event: {}", err).into(),
|
||
);
|
||
web_sys::window()
|
||
.unwrap()
|
||
.alert_with_message(&format!("Failed to update event: {}", err))
|
||
.unwrap();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
},
|
||
)
|
||
};
|
||
|
||
let refresh_calendars = {
|
||
let auth_token = auth_token.clone();
|
||
let user_info = user_info.clone();
|
||
Callback::from(move |_| {
|
||
if let Some(token) = (*auth_token).clone() {
|
||
let user_info = user_info.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.fetch_user_info(&token, &password).await {
|
||
Ok(mut info) => {
|
||
if let Ok(saved_colors_json) =
|
||
LocalStorage::get::<String>("calendar_colors")
|
||
{
|
||
if let Ok(saved_info) =
|
||
serde_json::from_str::<UserInfo>(&saved_colors_json)
|
||
{
|
||
for saved_cal in &saved_info.calendars {
|
||
for cal in &mut info.calendars {
|
||
if cal.path == saved_cal.path {
|
||
cal.color = saved_cal.color.clone();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
user_info.set(Some(info));
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::log_1(
|
||
&format!("Failed to refresh calendars: {}", err).into(),
|
||
);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
})
|
||
};
|
||
|
||
// Debug logging
|
||
web_sys::console::log_1(
|
||
&format!("App rendering: auth_token = {:?}", auth_token.is_some()).into(),
|
||
);
|
||
|
||
html! {
|
||
<BrowserRouter>
|
||
<div class="app" onclick={on_outside_click}>
|
||
{
|
||
if auth_token.is_some() {
|
||
html! {
|
||
<>
|
||
<Sidebar
|
||
user_info={(*user_info).clone()}
|
||
on_logout={on_logout}
|
||
on_add_calendar={Callback::from({
|
||
let calendar_management_modal_open = calendar_management_modal_open.clone();
|
||
move |_| calendar_management_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(mut events) = CalendarService::fetch_external_calendar_events(cal.id).await {
|
||
// Set calendar_path for color matching
|
||
for event in &mut events {
|
||
event.calendar_path = Some(format!("external_{}", cal.id));
|
||
}
|
||
all_events.extend(events);
|
||
}
|
||
}
|
||
}
|
||
external_calendar_events.set(all_events);
|
||
}
|
||
});
|
||
}
|
||
})}
|
||
on_external_calendar_delete={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 {
|
||
// Delete the external calendar from the server
|
||
if let Err(err) = CalendarService::delete_external_calendar(id).await {
|
||
web_sys::console::log_1(
|
||
&format!("Failed to delete external calendar: {}", err).into(),
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Remove calendar from local state
|
||
let mut calendars = (*external_calendars).clone();
|
||
calendars.retain(|c| c.id != id);
|
||
external_calendars.set(calendars.clone());
|
||
|
||
// Remove events from this calendar
|
||
let mut events = (*external_calendar_events).clone();
|
||
events.retain(|e| {
|
||
if let Some(ref calendar_path) = e.calendar_path {
|
||
calendar_path != &format!("external_{}", id)
|
||
} else {
|
||
true
|
||
}
|
||
});
|
||
external_calendar_events.set(events);
|
||
});
|
||
}
|
||
})}
|
||
on_external_calendar_refresh={Callback::from({
|
||
let external_calendar_events = external_calendar_events.clone();
|
||
let external_calendars = external_calendars.clone();
|
||
let refreshing_calendar_id = refreshing_calendar_id.clone();
|
||
move |id: i32| {
|
||
let external_calendar_events = external_calendar_events.clone();
|
||
let external_calendars = external_calendars.clone();
|
||
let refreshing_calendar_id = refreshing_calendar_id.clone();
|
||
|
||
// Set loading state
|
||
refreshing_calendar_id.set(Some(id));
|
||
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
web_sys::console::log_1(&format!("🔄 Refreshing external calendar {}", id).into());
|
||
|
||
// Force refresh of this specific calendar
|
||
match CalendarService::fetch_external_calendar_events(id).await {
|
||
Ok(mut events) => {
|
||
web_sys::console::log_1(&format!("✅ Successfully refreshed calendar {} with {} events", id, events.len()).into());
|
||
|
||
// Set calendar_path for color matching
|
||
for event in &mut events {
|
||
event.calendar_path = Some(format!("external_{}", id));
|
||
}
|
||
|
||
// Update events for this calendar
|
||
let mut all_events = (*external_calendar_events).clone();
|
||
// Remove old events from this calendar
|
||
all_events.retain(|e| {
|
||
if let Some(ref calendar_path) = e.calendar_path {
|
||
calendar_path != &format!("external_{}", id)
|
||
} else {
|
||
true
|
||
}
|
||
});
|
||
// Add new events
|
||
all_events.extend(events);
|
||
external_calendar_events.set(all_events);
|
||
|
||
// Update the last_fetched timestamp in calendars list
|
||
match CalendarService::get_external_calendars().await {
|
||
Ok(calendars) => {
|
||
external_calendars.set(calendars);
|
||
web_sys::console::log_1(&"✅ Calendar list updated with new timestamps".into());
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::error_1(&format!("⚠️ Failed to update calendar list: {}", err).into());
|
||
}
|
||
}
|
||
|
||
// Clear loading state on success
|
||
refreshing_calendar_id.set(None);
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::error_1(&format!("❌ Failed to refresh calendar {}: {}", id, err).into());
|
||
// Show error to user
|
||
if let Some(window) = web_sys::window() {
|
||
let _ = window.alert_with_message(&format!("Failed to refresh calendar: {}", err));
|
||
}
|
||
|
||
// Clear loading state on error
|
||
refreshing_calendar_id.set(None);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
})}
|
||
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).clone()}
|
||
refreshing_calendar_id={(*refreshing_calendar_id).clone()}
|
||
on_calendar_context_menu={on_calendar_context_menu}
|
||
on_calendar_visibility_toggle={Callback::from({
|
||
let user_info = user_info.clone();
|
||
move |calendar_path: String| {
|
||
let user_info = user_info.clone();
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
if let Some(mut info) = (*user_info).clone() {
|
||
// Toggle the visibility
|
||
if let Some(calendar) = info.calendars.iter_mut().find(|c| c.path == calendar_path) {
|
||
calendar.is_visible = !calendar.is_visible;
|
||
user_info.set(Some(info));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
})}
|
||
current_view={(*current_view).clone()}
|
||
on_view_change={on_view_change}
|
||
current_theme={(*current_theme).clone()}
|
||
on_theme_change={on_theme_change}
|
||
current_style={(*current_style).clone()}
|
||
on_style_change={on_style_change}
|
||
/>
|
||
<main class="app-main">
|
||
<RouteHandler
|
||
auth_token={(*auth_token).clone()}
|
||
user_info={(*user_info).clone()}
|
||
on_login={on_login.clone()}
|
||
external_calendar_events={(*external_calendar_events).clone()}
|
||
external_calendars={(*external_calendars).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()}
|
||
on_create_event_request={Some(on_event_create.clone())}
|
||
on_event_update_request={Some(on_event_update.clone())}
|
||
context_menus_open={any_context_menu_open}
|
||
/>
|
||
</main>
|
||
</>
|
||
}
|
||
} else {
|
||
html! {
|
||
<div class="login-layout">
|
||
<RouteHandler
|
||
auth_token={(*auth_token).clone()}
|
||
user_info={(*user_info).clone()}
|
||
on_login={on_login.clone()}
|
||
on_event_context_menu={Some(on_event_context_menu.clone())}
|
||
on_calendar_context_menu={Some(on_calendar_date_context_menu.clone())}
|
||
on_event_update_request={Some(on_event_update.clone())}
|
||
on_create_event_request={Some(on_event_create.clone())}
|
||
context_menus_open={any_context_menu_open}
|
||
/>
|
||
</div>
|
||
}
|
||
}
|
||
}
|
||
|
||
<CalendarManagementModal
|
||
is_open={*calendar_management_modal_open}
|
||
on_close={Callback::from({
|
||
let calendar_management_modal_open = calendar_management_modal_open.clone();
|
||
move |_| calendar_management_modal_open.set(false)
|
||
})}
|
||
on_create_calendar={Callback::from({
|
||
let auth_token = auth_token.clone();
|
||
let refresh_calendars = refresh_calendars.clone();
|
||
let calendar_management_modal_open = calendar_management_modal_open.clone();
|
||
move |(name, description, color): (String, Option<String>, Option<String>)| {
|
||
if let Some(token) = (*auth_token).clone() {
|
||
let refresh_calendars = refresh_calendars.clone();
|
||
let calendar_management_modal_open = calendar_management_modal_open.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.create_calendar(&token, &password, name, description, color).await {
|
||
Ok(_) => {
|
||
web_sys::console::log_1(&"Calendar created successfully!".into());
|
||
refresh_calendars.emit(());
|
||
calendar_management_modal_open.set(false);
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::log_1(&format!("Failed to create calendar: {}", err).into());
|
||
calendar_management_modal_open.set(false);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
})}
|
||
on_external_success={Callback::from({
|
||
let external_calendars = external_calendars.clone();
|
||
let calendar_management_modal_open = calendar_management_modal_open.clone();
|
||
move |new_id: i32| {
|
||
// Refresh external calendars list
|
||
let external_calendars = external_calendars.clone();
|
||
let calendar_management_modal_open = calendar_management_modal_open.clone();
|
||
|
||
wasm_bindgen_futures::spawn_local(async move {
|
||
let calendar_service = CalendarService::new();
|
||
match CalendarService::get_external_calendars().await {
|
||
Ok(calendars) => {
|
||
external_calendars.set(calendars);
|
||
calendar_management_modal_open.set(false);
|
||
web_sys::console::log_1(&format!("External calendar {} added successfully!", new_id).into());
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::error_1(&format!("Failed to refresh external calendars: {}", err).into());
|
||
calendar_management_modal_open.set(false);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
})}
|
||
available_colors={available_colors.iter().map(|c| c.to_string()).collect::<Vec<_>>()}
|
||
/>
|
||
|
||
<ContextMenu
|
||
is_open={*context_menu_open}
|
||
x={context_menu_pos.0}
|
||
y={context_menu_pos.1}
|
||
on_close={Callback::from({
|
||
let context_menu_open = context_menu_open.clone();
|
||
move |_| context_menu_open.set(false)
|
||
})}
|
||
on_delete={Callback::from({
|
||
let auth_token = auth_token.clone();
|
||
let context_menu_calendar_path = context_menu_calendar_path.clone();
|
||
let refresh_calendars = refresh_calendars.clone();
|
||
move |_: MouseEvent| {
|
||
if let (Some(token), Some(calendar_path)) = ((*auth_token).clone(), (*context_menu_calendar_path).clone()) {
|
||
let refresh_calendars = refresh_calendars.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.delete_calendar(&token, &password, calendar_path).await {
|
||
Ok(_) => {
|
||
web_sys::console::log_1(&"Calendar deleted successfully!".into());
|
||
refresh_calendars.emit(());
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::log_1(&format!("Failed to delete calendar: {}", err).into());
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
})}
|
||
/>
|
||
|
||
<EventContextMenu
|
||
is_open={*event_context_menu_open}
|
||
x={event_context_menu_pos.0}
|
||
y={event_context_menu_pos.1}
|
||
event={(*event_context_menu_event).clone()}
|
||
on_close={Callback::from({
|
||
let event_context_menu_open = event_context_menu_open.clone();
|
||
move |_| event_context_menu_open.set(false)
|
||
})}
|
||
on_edit={Callback::from({
|
||
let _event_context_menu_event = event_context_menu_event.clone();
|
||
let event_context_menu_open = event_context_menu_open.clone();
|
||
let create_event_modal_open = create_event_modal_open.clone();
|
||
let event_edit_scope = event_edit_scope.clone();
|
||
move |edit_action: EditAction| {
|
||
// Set the edit scope and close the context menu
|
||
event_edit_scope.set(Some(edit_action));
|
||
event_context_menu_open.set(false);
|
||
create_event_modal_open.set(true);
|
||
}
|
||
})}
|
||
on_delete={Callback::from({
|
||
let auth_token = auth_token.clone();
|
||
let event_context_menu_event = event_context_menu_event.clone();
|
||
let event_context_menu_open = event_context_menu_open.clone();
|
||
let refresh_calendar_data = refresh_calendar_data.clone();
|
||
move |delete_action: DeleteAction| {
|
||
if let (Some(token), Some(event)) = ((*auth_token).clone(), (*event_context_menu_event).clone()) {
|
||
let refresh_calendar_data = refresh_calendar_data.clone();
|
||
let event_context_menu_open = event_context_menu_open.clone();
|
||
|
||
// Log the delete action for now - we'll implement different behaviors later
|
||
match delete_action {
|
||
DeleteAction::DeleteThis => web_sys::console::log_1(&"Delete this event".into()),
|
||
DeleteAction::DeleteFollowing => web_sys::console::log_1(&"Delete following events".into()),
|
||
DeleteAction::DeleteSeries => web_sys::console::log_1(&"Delete entire series".into()),
|
||
}
|
||
|
||
let refresh_callback = refresh_calendar_data.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()
|
||
};
|
||
|
||
if let (Some(calendar_path), Some(event_href)) = (&event.calendar_path, &event.href) {
|
||
// Convert delete action to string and get occurrence date
|
||
let action_str = match delete_action {
|
||
DeleteAction::DeleteThis => "delete_this".to_string(),
|
||
DeleteAction::DeleteFollowing => "delete_following".to_string(),
|
||
DeleteAction::DeleteSeries => "delete_series".to_string(),
|
||
};
|
||
|
||
// Get the occurrence date from the clicked event
|
||
let occurrence_date = Some(event.dtstart.date_naive().format("%Y-%m-%d").to_string());
|
||
|
||
web_sys::console::log_1(&format!("🔄 Delete action: {}", action_str).into());
|
||
web_sys::console::log_1(&format!("🔄 Event UID: {}", event.uid).into());
|
||
web_sys::console::log_1(&format!("🔄 Event start: {}", event.dtstart).into());
|
||
web_sys::console::log_1(&format!("🔄 Occurrence date: {:?}", occurrence_date).into());
|
||
|
||
match calendar_service.delete_event(
|
||
&token,
|
||
&password,
|
||
calendar_path.clone(),
|
||
event_href.clone(),
|
||
action_str,
|
||
occurrence_date
|
||
).await {
|
||
Ok(message) => {
|
||
web_sys::console::log_1(&format!("Delete response: {}", message).into());
|
||
|
||
// Show the message to the user to explain what actually happened
|
||
if message.contains("Warning") {
|
||
web_sys::window().unwrap().alert_with_message(&message).unwrap();
|
||
}
|
||
|
||
// Close the context menu
|
||
event_context_menu_open.set(false);
|
||
// Refresh calendar data without page reload
|
||
refresh_callback.emit(());
|
||
}
|
||
Err(err) => {
|
||
web_sys::console::log_1(&format!("Failed to delete event: {}", err).into());
|
||
web_sys::window().unwrap().alert_with_message(&format!("Failed to delete event: {}", err)).unwrap();
|
||
}
|
||
}
|
||
} else {
|
||
web_sys::console::log_1(&"Missing calendar_path or href - cannot delete event".into());
|
||
}
|
||
});
|
||
}
|
||
}
|
||
})}
|
||
on_view_details={Callback::from({
|
||
let event_context_menu_open = event_context_menu_open.clone();
|
||
let view_event_modal_open = view_event_modal_open.clone();
|
||
let view_event_modal_event = view_event_modal_event.clone();
|
||
move |event: VEvent| {
|
||
// Set the event for viewing (read-only mode)
|
||
view_event_modal_event.set(Some(event));
|
||
event_context_menu_open.set(false);
|
||
view_event_modal_open.set(true);
|
||
}
|
||
})}
|
||
/>
|
||
|
||
<CalendarContextMenu
|
||
is_open={*calendar_context_menu_open}
|
||
x={calendar_context_menu_pos.0}
|
||
y={calendar_context_menu_pos.1}
|
||
on_close={Callback::from({
|
||
let calendar_context_menu_open = calendar_context_menu_open.clone();
|
||
move |_| calendar_context_menu_open.set(false)
|
||
})}
|
||
on_create_event={on_create_event_click}
|
||
/>
|
||
|
||
<CreateEventModal
|
||
is_open={*create_event_modal_open}
|
||
selected_date={(*selected_date_for_event).clone()}
|
||
initial_start_time={None}
|
||
initial_end_time={None}
|
||
event_to_edit={(*event_context_menu_event).clone()}
|
||
edit_scope={(*event_edit_scope).clone()}
|
||
on_close={Callback::from({
|
||
let create_event_modal_open = create_event_modal_open.clone();
|
||
let event_context_menu_event = event_context_menu_event.clone();
|
||
let event_edit_scope = event_edit_scope.clone();
|
||
move |_| {
|
||
create_event_modal_open.set(false);
|
||
// Clear the event being edited and edit scope
|
||
event_context_menu_event.set(None);
|
||
event_edit_scope.set(None);
|
||
}
|
||
})}
|
||
on_create={on_event_create}
|
||
available_calendars={user_info.as_ref().map(|ui| ui.calendars.clone()).unwrap_or_default()}
|
||
/>
|
||
|
||
|
||
<EventModal
|
||
event={if *view_event_modal_open { (*view_event_modal_event).clone() } else { None }}
|
||
on_close={Callback::from({
|
||
let view_event_modal_open = view_event_modal_open.clone();
|
||
let view_event_modal_event = view_event_modal_event.clone();
|
||
move |_| {
|
||
view_event_modal_open.set(false);
|
||
view_event_modal_event.set(None);
|
||
}
|
||
})}
|
||
/>
|
||
|
||
// Mobile warning modal
|
||
<MobileWarningModal
|
||
is_open={*mobile_warning_open}
|
||
on_close={on_mobile_warning_close}
|
||
/>
|
||
</div>
|
||
</BrowserRouter>
|
||
}
|
||
}
|