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:
@@ -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") {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user