Add external calendars feature: display read-only ICS calendars alongside CalDAV calendars

- Database: Add external_calendars table with user relationships and CRUD operations
- Backend: Implement REST API endpoints for external calendar management and ICS fetching
- Frontend: Add external calendar modal, sidebar section with visibility toggles
- Calendar integration: Merge external events with regular events in unified view
- ICS parsing: Support multiple datetime formats, recurring events, and timezone handling
- Authentication: Integrate with existing JWT token system for user-specific calendars
- UI: Visual distinction with 📅 indicator and separate management section

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-03 18:22:52 -04:00
parent 289284a532
commit 8caa1f45ae
16 changed files with 1207 additions and 18 deletions

View File

@@ -99,6 +99,38 @@ pub struct UserPreferences {
pub updated_at: DateTime<Utc>,
}
/// External calendar model
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct ExternalCalendar {
pub id: i32,
pub user_id: String,
pub name: String,
pub url: String,
pub color: String,
pub is_visible: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_fetched: Option<DateTime<Utc>>,
}
impl ExternalCalendar {
/// Create a new external calendar
pub fn new(user_id: String, name: String, url: String, color: String) -> Self {
let now = Utc::now();
Self {
id: 0, // Will be set by database
user_id,
name,
url,
color,
is_visible: true,
created_at: now,
updated_at: now,
last_fetched: None,
}
}
}
impl UserPreferences {
/// Create default preferences for a new user
pub fn default_for_user(user_id: String) -> Self {
@@ -308,6 +340,91 @@ impl<'a> PreferencesRepository<'a> {
.execute(self.db.pool())
.await?;
Ok(())
}
}
/// Repository for ExternalCalendar operations
pub struct ExternalCalendarRepository<'a> {
db: &'a Database,
}
impl<'a> ExternalCalendarRepository<'a> {
pub fn new(db: &'a Database) -> Self {
Self { db }
}
/// Get all external calendars for a user
pub async fn get_by_user(&self, user_id: &str) -> Result<Vec<ExternalCalendar>> {
sqlx::query_as::<_, ExternalCalendar>(
"SELECT * FROM external_calendars WHERE user_id = ? ORDER BY created_at ASC",
)
.bind(user_id)
.fetch_all(self.db.pool())
.await
}
/// Create a new external calendar
pub async fn create(&self, calendar: &ExternalCalendar) -> Result<i32> {
let result = sqlx::query(
"INSERT INTO external_calendars (user_id, name, url, color, is_visible, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)",
)
.bind(&calendar.user_id)
.bind(&calendar.name)
.bind(&calendar.url)
.bind(&calendar.color)
.bind(&calendar.is_visible)
.bind(&calendar.created_at)
.bind(&calendar.updated_at)
.execute(self.db.pool())
.await?;
Ok(result.last_insert_rowid() as i32)
}
/// Update an external calendar
pub async fn update(&self, id: i32, calendar: &ExternalCalendar) -> Result<()> {
sqlx::query(
"UPDATE external_calendars
SET name = ?, url = ?, color = ?, is_visible = ?, updated_at = ?
WHERE id = ? AND user_id = ?",
)
.bind(&calendar.name)
.bind(&calendar.url)
.bind(&calendar.color)
.bind(&calendar.is_visible)
.bind(Utc::now())
.bind(id)
.bind(&calendar.user_id)
.execute(self.db.pool())
.await?;
Ok(())
}
/// Delete an external calendar
pub async fn delete(&self, id: i32, user_id: &str) -> Result<()> {
sqlx::query("DELETE FROM external_calendars WHERE id = ? AND user_id = ?")
.bind(id)
.bind(user_id)
.execute(self.db.pool())
.await?;
Ok(())
}
/// Update last_fetched timestamp
pub async fn update_last_fetched(&self, id: i32, user_id: &str) -> Result<()> {
sqlx::query(
"UPDATE external_calendars SET last_fetched = ? WHERE id = ? AND user_id = ?",
)
.bind(Utc::now())
.bind(id)
.bind(user_id)
.execute(self.db.pool())
.await?;
Ok(())
}
}