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:
		| @@ -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(()) | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone