use crate::models::{ClientEvent, Sermon, Bulletin, BibleVerse}; use chrono::{DateTime, Utc, NaiveDateTime}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeedItem { pub id: String, pub feed_type: FeedItemType, pub timestamp: String, // ISO8601 format pub priority: i32, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum FeedItemType { #[serde(rename = "event")] Event { event: ClientEvent, }, #[serde(rename = "sermon")] Sermon { sermon: Sermon, }, #[serde(rename = "bulletin")] Bulletin { bulletin: Bulletin, }, #[serde(rename = "verse")] Verse { verse: BibleVerse, }, } /// Parse date string to DateTime, with fallback to current time fn parse_date_with_fallback(date_str: &str) -> DateTime { // Try ISO8601 format first if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) { return dt.with_timezone(&Utc); } // Try naive datetime parsing if let Ok(naive) = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S") { return DateTime::from_naive_utc_and_offset(naive, Utc); } // Fallback to current time Utc::now() } /// Calculate priority for feed items based on type and recency fn calculate_priority(feed_type: &FeedItemType, timestamp: &DateTime) -> i32 { let now = Utc::now(); let age_days = (now - *timestamp).num_days().max(0); match feed_type { FeedItemType::Event { .. } => { // Events get highest priority, especially upcoming ones if *timestamp > now { 1000 // Future events (upcoming) } else { 800 - (age_days as i32) // Recent past events } }, FeedItemType::Sermon { .. } => { // Sermons get high priority when recent 600 - (age_days as i32) }, FeedItemType::Bulletin { .. } => { // Bulletins get medium priority 400 - (age_days as i32) }, FeedItemType::Verse { .. } => { // Daily verse always gets consistent priority 300 }, } } /// Aggregate and sort home feed items pub fn aggregate_home_feed( events: &[ClientEvent], sermons: &[Sermon], bulletins: &[Bulletin], daily_verse: Option<&BibleVerse> ) -> Vec { let mut feed_items = Vec::new(); // Add recent sermons (limit to 3) for sermon in sermons.iter().take(3) { let timestamp = sermon.date; // Already a DateTime let feed_type = FeedItemType::Sermon { sermon: sermon.clone() }; let priority = calculate_priority(&feed_type, ×tamp); feed_items.push(FeedItem { id: format!("sermon_{}", sermon.id), feed_type, timestamp: timestamp.to_rfc3339(), priority, }); } // Add upcoming events (limit to 2) for event in events.iter().take(2) { let timestamp = parse_date_with_fallback(&event.created_at); let feed_type = FeedItemType::Event { event: event.clone() }; let priority = calculate_priority(&feed_type, ×tamp); feed_items.push(FeedItem { id: format!("event_{}", event.id), feed_type, timestamp: timestamp.to_rfc3339(), priority, }); } // Add most recent bulletin if let Some(bulletin) = bulletins.first() { let timestamp = parse_date_with_fallback(&bulletin.date.to_string()); let feed_type = FeedItemType::Bulletin { bulletin: bulletin.clone() }; let priority = calculate_priority(&feed_type, ×tamp); feed_items.push(FeedItem { id: format!("bulletin_{}", bulletin.id), feed_type, timestamp: timestamp.to_rfc3339(), priority, }); } // Add daily verse if let Some(verse) = daily_verse { let timestamp = Utc::now(); let feed_type = FeedItemType::Verse { verse: verse.clone() }; let priority = calculate_priority(&feed_type, ×tamp); feed_items.push(FeedItem { id: format!("verse_{}", verse.reference), feed_type, timestamp: timestamp.to_rfc3339(), priority, }); } // Sort by priority (highest first), then by timestamp (newest first) feed_items.sort_by(|a, b| { b.priority.cmp(&a.priority) .then_with(|| b.timestamp.cmp(&a.timestamp)) }); feed_items } /// Media type enumeration for content categorization #[derive(Debug, Clone, Serialize, Deserialize)] pub enum MediaType { Sermons, LiveStreams, } impl MediaType { pub fn display_name(&self) -> &'static str { match self { MediaType::Sermons => "Sermons", MediaType::LiveStreams => "Live Archives", } } pub fn icon_name(&self) -> &'static str { match self { MediaType::Sermons => "play.rectangle.fill", MediaType::LiveStreams => "dot.radiowaves.left.and.right", } } } /// Get sermons or livestreams based on media type pub fn get_media_content(sermons: &[Sermon], media_type: &MediaType) -> Vec { match media_type { MediaType::Sermons => { // Filter for regular sermons (non-livestream) sermons.iter() .filter(|sermon| !sermon.title.to_lowercase().contains("livestream")) .cloned() .collect() }, MediaType::LiveStreams => { // Filter for livestream archives sermons.iter() .filter(|sermon| sermon.title.to_lowercase().contains("livestream")) .cloned() .collect() }, } } #[cfg(test)] mod tests { use super::*; use crate::models::{ClientEvent, Sermon, Bulletin, BibleVerse}; fn create_sample_event(id: &str, title: &str) -> ClientEvent { ClientEvent { id: id.to_string(), title: title.to_string(), description: "Sample description".to_string(), date: "2025-01-15".to_string(), start_time: "6:00 PM".to_string(), end_time: "8:00 PM".to_string(), location: "Sample Location".to_string(), location_url: None, image: None, thumbnail: None, category: "Social".to_string(), is_featured: false, recurring_type: None, tags: None, contact_email: None, contact_phone: None, registration_url: None, max_attendees: None, current_attendees: None, created_at: "2025-01-10T10:00:00Z".to_string(), updated_at: "2025-01-10T10:00:00Z".to_string(), duration_minutes: 120, has_registration: false, is_full: false, spots_remaining: None, } } fn create_sample_sermon(id: &str, title: &str) -> Sermon { Sermon { id: id.to_string(), title: title.to_string(), description: Some("Sample sermon".to_string()), date: Some("2025-01-10T10:00:00Z".to_string()), video_url: Some("https://example.com/video".to_string()), audio_url: None, thumbnail_url: None, duration: None, speaker: Some("Pastor Smith".to_string()), series: None, scripture_references: None, tags: None, } } #[test] fn test_aggregate_home_feed() { let events = vec![ create_sample_event("1", "Event 1"), create_sample_event("2", "Event 2"), ]; let sermons = vec![ create_sample_sermon("1", "Sermon 1"), create_sample_sermon("2", "Sermon 2"), ]; let bulletins = vec![ Bulletin { id: "1".to_string(), title: "Weekly Bulletin".to_string(), date: "2025-01-12T10:00:00Z".to_string(), pdf_url: "https://example.com/bulletin.pdf".to_string(), description: Some("This week's bulletin".to_string()), thumbnail_url: None, } ]; let verse = BibleVerse { text: "For God so loved the world...".to_string(), reference: "John 3:16".to_string(), version: Some("KJV".to_string()), }; let feed = aggregate_home_feed(&events, &sermons, &bulletins, Some(&verse)); assert!(feed.len() >= 4); // Should have events, sermons, bulletin, and verse // Check that items are sorted by priority for i in 1..feed.len() { assert!(feed[i-1].priority >= feed[i].priority); } } #[test] fn test_media_type_display() { assert_eq!(MediaType::Sermons.display_name(), "Sermons"); assert_eq!(MediaType::LiveStreams.display_name(), "Live Archives"); assert_eq!(MediaType::Sermons.icon_name(), "play.rectangle.fill"); assert_eq!(MediaType::LiveStreams.icon_name(), "dot.radiowaves.left.and.right"); } #[test] fn test_get_media_content() { let sermons = vec![ create_sample_sermon("1", "Regular Sermon"), create_sample_sermon("2", "Livestream Service"), create_sample_sermon("3", "Another Sermon"), ]; let regular_sermons = get_media_content(&sermons, &MediaType::Sermons); assert_eq!(regular_sermons.len(), 2); let livestreams = get_media_content(&sermons, &MediaType::LiveStreams); assert_eq!(livestreams.len(), 1); assert!(livestreams[0].title.contains("Livestream")); } }