Add user information and calendar list to sidebar
Backend changes: - Add /api/user/info endpoint to fetch user details and calendar list - Create UserInfo and CalendarInfo models for API responses - Filter out generic calendar collections from sidebar display - Extract readable calendar names with proper title case formatting Frontend changes: - Fetch and display user info (username, server URL) in sidebar header - Show list of user's calendars with hover effects and styling - Add loading states and error handling for user info - Reorganize sidebar layout: header, navigation, calendar list, logout Styling: - Enhanced sidebar with user info section and calendar list - Responsive design hides user info and calendar list on mobile - Improved logout button positioning in sidebar footer - Professional styling with proper spacing and visual hierarchy 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										91
									
								
								src/app.rs
									
									
									
									
									
								
							
							
						
						
									
										91
									
								
								src/app.rs
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ use yew::prelude::*; | ||||
| use yew_router::prelude::*; | ||||
| use gloo_storage::{LocalStorage, Storage}; | ||||
| use crate::components::{Login, Calendar}; | ||||
| use crate::services::{CalendarService, CalendarEvent}; | ||||
| use crate::services::{CalendarService, CalendarEvent, UserInfo, CalendarInfo}; | ||||
| use std::collections::HashMap; | ||||
| use chrono::{Local, NaiveDate, Datelike}; | ||||
|  | ||||
| @@ -21,6 +21,8 @@ pub fn App() -> Html { | ||||
|     let auth_token = use_state(|| -> Option<String> { | ||||
|         LocalStorage::get("auth_token").ok() | ||||
|     }); | ||||
|      | ||||
|     let user_info = use_state(|| -> Option<UserInfo> { None }); | ||||
|  | ||||
|     let on_login = { | ||||
|         let auth_token = auth_token.clone(); | ||||
| @@ -31,12 +33,57 @@ pub fn App() -> Html { | ||||
|  | ||||
|     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); | ||||
|         }) | ||||
|     }; | ||||
|  | ||||
|     // 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(); | ||||
|                      | ||||
|                     // Get password from stored credentials | ||||
|                     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(info) => { | ||||
|                                 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); | ||||
|             } | ||||
|              | ||||
|             || () | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     html! { | ||||
|         <BrowserRouter> | ||||
|             <div class="app"> | ||||
| @@ -47,11 +94,51 @@ pub fn App() -> Html { | ||||
|                                 <aside class="app-sidebar"> | ||||
|                                     <div class="sidebar-header"> | ||||
|                                         <h1>{"Calendar App"}</h1> | ||||
|                                         { | ||||
|                                             if let Some(ref info) = *user_info { | ||||
|                                                 html! { | ||||
|                                                     <div class="user-info"> | ||||
|                                                         <div class="username">{&info.username}</div> | ||||
|                                                         <div class="server-url">{&info.server_url}</div> | ||||
|                                                     </div> | ||||
|                                                 } | ||||
|                                             } else { | ||||
|                                                 html! { <div class="user-info loading">{"Loading..."}</div> } | ||||
|                                             } | ||||
|                                         } | ||||
|                                     </div> | ||||
|                                     <nav class="sidebar-nav"> | ||||
|                                         <Link<Route> to={Route::Calendar} classes="nav-link">{"Calendar"}</Link<Route>> | ||||
|                                         <button onclick={on_logout} class="logout-button">{"Logout"}</button> | ||||
|                                     </nav> | ||||
|                                     { | ||||
|                                         if let Some(ref info) = *user_info { | ||||
|                                             if !info.calendars.is_empty() { | ||||
|                                                 html! { | ||||
|                                                     <div class="calendar-list"> | ||||
|                                                         <h3>{"My Calendars"}</h3> | ||||
|                                                         <ul> | ||||
|                                                             { | ||||
|                                                                 info.calendars.iter().map(|cal| { | ||||
|                                                                     html! { | ||||
|                                                                         <li key={cal.path.clone()}> | ||||
|                                                                             <span class="calendar-name">{&cal.display_name}</span> | ||||
|                                                                         </li> | ||||
|                                                                     } | ||||
|                                                                 }).collect::<Html>() | ||||
|                                                             } | ||||
|                                                         </ul> | ||||
|                                                     </div> | ||||
|                                                 } | ||||
|                                             } else { | ||||
|                                                 html! { <div class="no-calendars">{"No calendars found"}</div> } | ||||
|                                             } | ||||
|                                         } else { | ||||
|                                             html! {} | ||||
|                                         } | ||||
|                                     } | ||||
|                                     <div class="sidebar-footer"> | ||||
|                                         <button onclick={on_logout} class="logout-button">{"Logout"}</button> | ||||
|                                     </div> | ||||
|                                 </aside> | ||||
|                                 <main class="app-main"> | ||||
|                                     <Switch<Route> render={move |route| { | ||||
|   | ||||
| @@ -25,6 +25,19 @@ impl Default for ReminderAction { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct UserInfo { | ||||
|     pub username: String, | ||||
|     pub server_url: String, | ||||
|     pub calendars: Vec<CalendarInfo>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct CalendarInfo { | ||||
|     pub path: String, | ||||
|     pub display_name: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct CalendarEvent { | ||||
|     pub uid: String, | ||||
| @@ -135,6 +148,48 @@ impl CalendarService { | ||||
|         Self { base_url } | ||||
|     } | ||||
|  | ||||
|     /// Fetch user info including available calendars | ||||
|     pub async fn fetch_user_info(&self, token: &str, password: &str) -> Result<UserInfo, 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 url = format!("{}/user/info", self.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("X-CalDAV-Password", password) | ||||
|             .map_err(|e| format!("Password header setting failed: {:?}", e))?; | ||||
|  | ||||
|         let resp_value = JsFuture::from(window.fetch_with_request(&request)) | ||||
|             .await | ||||
|             .map_err(|e| format!("Network request failed: {:?}", e))?; | ||||
|  | ||||
|         let resp: Response = resp_value.dyn_into() | ||||
|             .map_err(|e| format!("Response cast failed: {:?}", e))?; | ||||
|  | ||||
|         let text = JsFuture::from(resp.text() | ||||
|             .map_err(|e| format!("Text extraction failed: {:?}", e))?) | ||||
|             .await | ||||
|             .map_err(|e| format!("Text promise failed: {:?}", e))?; | ||||
|  | ||||
|         let text_string = text.as_string() | ||||
|             .ok_or("Response text is not a string")?; | ||||
|  | ||||
|         if resp.ok() { | ||||
|             let user_info: UserInfo = serde_json::from_str(&text_string) | ||||
|                 .map_err(|e| format!("JSON parsing failed: {}", e))?; | ||||
|             Ok(user_info) | ||||
|         } else { | ||||
|             Err(format!("Request failed with status {}: {}", resp.status(), text_string)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Fetch calendar events for a specific month | ||||
|     pub async fn fetch_events_for_month( | ||||
|         &self,  | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| pub mod calendar_service; | ||||
|  | ||||
| pub use calendar_service::{CalendarService, CalendarEvent, EventReminder, ReminderAction}; | ||||
| pub use calendar_service::{CalendarService, CalendarEvent, EventReminder, ReminderAction, UserInfo, CalendarInfo}; | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone