From aa7a15e6fa72881e7fac5442bee4a596fa89ee81 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Fri, 5 Sep 2025 11:46:21 -0400 Subject: [PATCH] Implement tabbed calendar management modal with improved styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/src/app.rs | 111 ++--- .../components/calendar_management_modal.rs | 401 ++++++++++++++++++ frontend/src/components/mod.rs | 4 +- frontend/src/components/sidebar.rs | 11 +- frontend/styles.css | 227 +++++++--- 5 files changed, 624 insertions(+), 130 deletions(-) create mode 100644 frontend/src/components/calendar_management_modal.rs diff --git a/frontend/src/app.rs b/frontend/src/app.rs index 4427519..c06d85a 100644 --- a/frontend/src/app.rs +++ b/frontend/src/app.rs @@ -1,6 +1,6 @@ use crate::components::{ - CalendarContextMenu, ContextMenu, CreateCalendarModal, CreateEventModal, DeleteAction, - EditAction, EventContextMenu, EventModal, EventCreationData, ExternalCalendarModal, + CalendarContextMenu, CalendarManagementModal, ContextMenu, CreateEventModal, DeleteAction, + EditAction, EventContextMenu, EventModal, EventCreationData, MobileWarningModal, RouteHandler, Sidebar, Theme, ViewMode, }; use crate::components::mobile_warning_modal::is_mobile_device; @@ -95,7 +95,7 @@ pub fn App() -> Html { let user_info = use_state(|| -> Option { None }); let color_picker_open = use_state(|| -> Option { None }); - let create_modal_open = use_state(|| false); + 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 { None }); @@ -121,7 +121,6 @@ pub fn App() -> Html { // Mobile warning state let mobile_warning_open = use_state(|| is_mobile_device()); - let external_calendar_modal_open = use_state(|| false); let refresh_interval = use_state(|| -> Option { None }); // Calendar view state - load from localStorage if available @@ -1168,13 +1167,9 @@ pub fn App() -> Html { Html { } } - , Option)| { if let Some(token) = (*auth_token).clone() { let refresh_calendars = refresh_calendars.clone(); - let create_modal_open = create_modal_open.clone(); + let calendar_management_modal_open = calendar_management_modal_open.clone(); wasm_bindgen_futures::spawn_local(async move { let calendar_service = CalendarService::new(); @@ -1418,17 +1413,41 @@ pub fn App() -> Html { Ok(_) => { web_sys::console::log_1(&"Calendar created successfully!".into()); refresh_calendars.emit(()); - create_modal_open.set(false); + calendar_management_modal_open.set(false); } Err(err) => { web_sys::console::log_1(&format!("Failed to create calendar: {}", err).into()); - create_modal_open.set(false); + 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::>()} /> @@ -1623,58 +1642,6 @@ pub fn App() -> Html { available_calendars={user_info.as_ref().map(|ui| ui.calendars.clone()).unwrap_or_default()} /> - { - external_calendars.set(calendars.clone()); - - // Then immediately fetch events for the new calendar if it's visible - if let Some(new_calendar) = calendars.iter().find(|c| c.id == new_calendar_id) { - if new_calendar.is_visible { - match CalendarService::fetch_external_calendar_events(new_calendar_id).await { - Ok(mut events) => { - // Set calendar_path for color matching - for event in &mut events { - event.calendar_path = Some(format!("external_{}", new_calendar_id)); - } - - // Add the new calendar's events to existing events - let mut all_events = (*external_calendar_events).clone(); - all_events.extend(events); - external_calendar_events.set(all_events); - } - Err(e) => { - web_sys::console::log_1( - &format!("Failed to fetch events for new calendar {}: {}", new_calendar_id, e).into(), - ); - } - } - } - } - } - Err(err) => { - web_sys::console::log_1( - &format!("Failed to refresh calendars after creation: {}", err).into(), - ); - } - } - }); - } - })} - /> , + pub on_create_calendar: Callback<(String, Option, Option)>, // name, description, color + pub on_external_success: Callback, // Pass the newly created external calendar ID + pub available_colors: Vec, +} + +#[function_component(CalendarManagementModal)] +pub fn calendar_management_modal(props: &CalendarManagementModalProps) -> Html { + let active_tab = use_state(|| CalendarTab::Create); + + // Create Calendar state + let calendar_name = use_state(|| String::new()); + let description = use_state(|| String::new()); + let selected_color = use_state(|| None::); + let create_error_message = use_state(|| None::); + let is_creating = use_state(|| false); + + // External Calendar state + let external_name_ref = use_node_ref(); + let external_url_ref = use_node_ref(); + let external_color_ref = use_node_ref(); + let external_is_loading = use_state(|| false); + let external_error_message = use_state(|| None::); + + // Reset state when modal opens + use_effect_with(props.is_open, { + let calendar_name = calendar_name.clone(); + let description = description.clone(); + let selected_color = selected_color.clone(); + let create_error_message = create_error_message.clone(); + let is_creating = is_creating.clone(); + let external_is_loading = external_is_loading.clone(); + let external_error_message = external_error_message.clone(); + let active_tab = active_tab.clone(); + + move |is_open| { + if *is_open { + // Reset all state when modal opens + calendar_name.set(String::new()); + description.set(String::new()); + selected_color.set(None); + create_error_message.set(None); + is_creating.set(false); + external_is_loading.set(false); + external_error_message.set(None); + active_tab.set(CalendarTab::Create); + } + } + }); + + let on_tab_click = { + let active_tab = active_tab.clone(); + Callback::from(move |tab: CalendarTab| { + active_tab.set(tab); + }) + }; + + let on_backdrop_click = { + let on_close = props.on_close.clone(); + Callback::from(move |e: MouseEvent| { + if let Some(target) = e.target() { + let element = target.dyn_into::().unwrap(); + if element.class_list().contains("modal-backdrop") { + on_close.emit(()); + } + } + }) + }; + + // Create Calendar handlers + let on_name_change = { + let calendar_name = calendar_name.clone(); + Callback::from(move |e: InputEvent| { + let input: HtmlInputElement = e.target_unchecked_into(); + calendar_name.set(input.value()); + }) + }; + + let on_description_change = { + let description = description.clone(); + Callback::from(move |e: InputEvent| { + let input: web_sys::HtmlTextAreaElement = e.target_unchecked_into(); + description.set(input.value()); + }) + }; + + let on_color_select = { + let selected_color = selected_color.clone(); + Callback::from(move |color: String| { + selected_color.set(Some(color)); + }) + }; + + let on_create_submit = { + let calendar_name = calendar_name.clone(); + let description = description.clone(); + let selected_color = selected_color.clone(); + let create_error_message = create_error_message.clone(); + let is_creating = is_creating.clone(); + let on_create_calendar = props.on_create_calendar.clone(); + + Callback::from(move |e: SubmitEvent| { + e.prevent_default(); + + let name = (*calendar_name).trim(); + if name.is_empty() { + create_error_message.set(Some("Calendar name is required".to_string())); + return; + } + + is_creating.set(true); + create_error_message.set(None); + + let desc = if description.is_empty() { + None + } else { + Some((*description).clone()) + }; + + on_create_calendar.emit((name.to_string(), desc, (*selected_color).clone())); + }) + }; + + // External Calendar handlers + let on_external_submit = { + let external_name_ref = external_name_ref.clone(); + let external_url_ref = external_url_ref.clone(); + let external_color_ref = external_color_ref.clone(); + let external_is_loading = external_is_loading.clone(); + let external_error_message = external_error_message.clone(); + let on_close = props.on_close.clone(); + let on_external_success = props.on_external_success.clone(); + + Callback::from(move |e: SubmitEvent| { + e.prevent_default(); + + let name = external_name_ref + .cast::() + .map(|input| input.value()) + .unwrap_or_default() + .trim() + .to_string(); + + let url = external_url_ref + .cast::() + .map(|input| input.value()) + .unwrap_or_default() + .trim() + .to_string(); + + let color = external_color_ref + .cast::() + .map(|input| input.value()) + .unwrap_or_else(|| "#4285f4".to_string()); + + if name.is_empty() || url.is_empty() { + external_error_message.set(Some("Name and URL are required".to_string())); + return; + } + + external_is_loading.set(true); + external_error_message.set(None); + + let external_is_loading = external_is_loading.clone(); + let external_error_message = external_error_message.clone(); + let on_close = on_close.clone(); + let on_external_success = on_external_success.clone(); + + wasm_bindgen_futures::spawn_local(async move { + let calendar_service = CalendarService::new(); + + match CalendarService::create_external_calendar(&name, &url, &color).await { + Ok(calendar) => { + external_is_loading.set(false); + on_close.emit(()); + on_external_success.emit(calendar.id); + } + Err(e) => { + external_is_loading.set(false); + external_error_message.set(Some(format!("Failed to add calendar: {}", e))); + } + } + }); + }) + }; + + if !props.is_open { + return html! {}; + } + + html! { +