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:
Connor Johnstone
2025-08-28 19:58:02 -04:00
parent 8a0d2286dc
commit 7c83a4522c
7 changed files with 338 additions and 10 deletions

View File

@@ -7,7 +7,7 @@ use serde::Deserialize;
use std::sync::Arc;
use chrono::Datelike;
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError}};
use crate::{AppState, models::{CalDAVLoginRequest, AuthResponse, ApiError, UserInfo, CalendarInfo}};
use crate::calendar::{CalDAVClient, CalendarEvent};
#[derive(Deserialize)]
@@ -116,6 +116,82 @@ pub async fn verify_token(
})))
}
pub async fn get_user_info(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<UserInfo>, ApiError> {
// Extract and verify token
let token = extract_bearer_token(&headers)?;
let password = extract_password_header(&headers)?;
let claims = state.auth_service.verify_token(&token)?;
// Create CalDAV config from token and password
let config = state.auth_service.caldav_config_from_token(&token, &password)?;
let client = CalDAVClient::new(config);
// Discover calendars
let calendar_paths = client.discover_calendars()
.await
.map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?;
// Convert paths to CalendarInfo structs with display names, filtering out generic collections
let calendars: Vec<CalendarInfo> = calendar_paths.into_iter()
.filter_map(|path| {
let display_name = extract_calendar_name(&path);
// Skip generic collection names
if display_name.eq_ignore_ascii_case("calendar") ||
display_name.eq_ignore_ascii_case("calendars") ||
display_name.eq_ignore_ascii_case("collection") {
None
} else {
Some(CalendarInfo {
path,
display_name,
})
}
}).collect();
Ok(Json(UserInfo {
username: claims.username,
server_url: claims.server_url,
calendars,
}))
}
// Helper function to extract a readable calendar name from path
fn extract_calendar_name(path: &str) -> String {
// Extract the last meaningful part of the path
// e.g., "/calendars/user/personal/" -> "personal"
// or "/calendars/user/work-calendar/" -> "work-calendar"
let parts: Vec<&str> = path.trim_end_matches('/').split('/').collect();
if let Some(last_part) = parts.last() {
if !last_part.is_empty() && *last_part != "calendars" {
// Convert kebab-case or snake_case to title case
last_part
.replace('-', " ")
.replace('_', " ")
.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
} else if parts.len() > 1 {
// If the last part is empty or "calendars", try the second to last
extract_calendar_name(&parts[..parts.len()-1].join("/"))
} else {
"Calendar".to_string()
}
} else {
"Calendar".to_string()
}
}
// Helper functions
fn extract_bearer_token(headers: &HeaderMap) -> Result<String, ApiError> {
if let Some(auth_header) = headers.get("authorization") {

View File

@@ -37,6 +37,7 @@ pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
.route("/api/health", get(health_check))
.route("/api/auth/login", post(handlers::login))
.route("/api/auth/verify", get(handlers::verify_token))
.route("/api/user/info", get(handlers::get_user_info))
.route("/api/calendar/events", get(handlers::get_calendar_events))
.route("/api/calendar/events/:uid", get(handlers::refresh_event))
.layer(

View File

@@ -20,6 +20,19 @@ pub struct AuthResponse {
pub server_url: String,
}
#[derive(Debug, Serialize)]
pub struct UserInfo {
pub username: String,
pub server_url: String,
pub calendars: Vec<CalendarInfo>,
}
#[derive(Debug, Serialize)]
pub struct CalendarInfo {
pub path: String,
pub display_name: String,
}
// Error handling
#[derive(Debug)]
pub enum ApiError {