From dbcbf9626f4fbe2b2046ed56b62a64137a2271e3 Mon Sep 17 00:00:00 2001 From: Benjamin Slingo Date: Sat, 30 Aug 2025 15:59:37 -0400 Subject: [PATCH] Fix date formatting and time range display for sermons and livestreams - Add shared date formatting function to eliminate DRY violations - Fix livestream cards showing raw ISO dates by using shared formatter - Update formatted_start_time() to return time range (start - end) for events - Switch events API to use v2 endpoint to avoid timezone conversion issues - Map duration_string to duration field for frontend compatibility --- src/api.rs | 100 ++++++++++++++++++++++++++++++++++++++++++-- src/models/event.rs | 11 +++-- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/api.rs b/src/api.rs index 1b8fc9e..c319f05 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,6 +1,7 @@ use crate::{ ChurchApiClient, ChurchCoreConfig, models::{NewSchedule, ScheduleUpdate, NewBulletin, BulletinUpdate, NewEvent, EventUpdate}, + utils::validation::{validate_event_form, validate_event_field, EventFormData, ValidationResult}, }; use tokio::runtime::Runtime; use std::sync::OnceLock; @@ -141,7 +142,7 @@ pub fn fetch_events_json() -> String { let client = get_client(); let rt = get_runtime(); - match rt.block_on(client.get_upcoming_events(Some(50))) { + match rt.block_on(client.get_upcoming_events_v2(Some(50))) { Ok(events) => { // Format events with display formatting using existing Event methods let formatted_events: Vec<_> = events.iter().map(|event| { @@ -171,16 +172,69 @@ pub fn fetch_featured_events_json() -> String { } } +// Shared function to format sermon-like items with consistent date formatting +fn format_sermon_items_with_dates(items: Vec) -> String +where + T: serde::Serialize, +{ + use serde_json::Value; + + let formatted_items: Vec<_> = items.iter().map(|item| { + let mut item_json = serde_json::to_value(item).unwrap_or_default(); + + // Format date and duration fields for frontend compatibility + if let Some(obj) = item_json.as_object_mut() { + // Handle date formatting + if let Some(date_value) = obj.get("date") { + // Try to parse as DateTime and format + if let Some(date_str) = date_value.as_str() { + // Try parsing ISO format first + if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(date_str) { + let formatted_date = datetime.format("%B %d, %Y").to_string(); + obj.insert("date".to_string(), Value::String(formatted_date)); + } else if let Ok(datetime) = chrono::DateTime::parse_from_str(&format!("{} 00:00:00 +0000", date_str), "%Y-%m-%d %H:%M:%S %z") { + let formatted_date = datetime.format("%B %d, %Y").to_string(); + obj.insert("date".to_string(), Value::String(formatted_date)); + } + } else if let Some(date_obj) = date_value.as_object() { + // Handle DateTime objects from serde serialization + if let (Some(secs), Some(nanos)) = (date_obj.get("secs_since_epoch"), date_obj.get("nanos_since_epoch")) { + if let (Some(secs), Some(nanos)) = (secs.as_i64(), nanos.as_u64()) { + if let Some(datetime) = chrono::DateTime::from_timestamp(secs, nanos as u32) { + let formatted_date = datetime.format("%B %d, %Y").to_string(); + obj.insert("date".to_string(), Value::String(formatted_date)); + } + } + } + } + } + + // Map duration_string to duration for frontend compatibility + if let Some(duration_string) = obj.get("duration_string").and_then(|v| v.as_str()) { + obj.insert("duration".to_string(), Value::String(duration_string.to_string())); + } + } + item_json + }).collect(); + + serde_json::to_string(&formatted_items).unwrap_or_else(|_| "[]".to_string()) +} + pub fn fetch_sermons_json() -> String { let client = get_client(); let rt = get_runtime(); match rt.block_on(client.get_recent_sermons(Some(20))) { - Ok(sermons) => serde_json::to_string(&sermons).unwrap_or_else(|_| "[]".to_string()), + Ok(sermons) => format_sermon_items_with_dates(sermons), Err(_) => "[]".to_string(), } } +pub fn parse_sermons_from_json(sermons_json: String) -> String { + // For compatibility with iOS - just pass through since we already format properly + sermons_json +} + pub fn fetch_config_json() -> String { let client = get_client(); let rt = get_runtime(); @@ -191,6 +245,22 @@ pub fn fetch_config_json() -> String { } } +pub fn update_config_json(config_json: String) -> String { + let client = get_client(); + let rt = get_runtime(); + + // Parse the JSON into a ChurchConfig + match serde_json::from_str(&config_json) { + Ok(config) => { + match rt.block_on(client.update_config(config)) { + Ok(_) => serde_json::to_string(&serde_json::json!({"success": true})).unwrap_or_else(|_| r#"{"success": false, "error": "JSON serialization failed"}"#.to_string()), + Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": e.to_string()})).unwrap_or_else(|_| r#"{"success": false, "error": "Unknown error"}"#.to_string()), + } + }, + Err(e) => serde_json::to_string(&serde_json::json!({"success": false, "error": format!("Invalid JSON: {}", e)})).unwrap_or_else(|_| r#"{"success": false, "error": "JSON parsing failed"}"#.to_string()), + } +} + pub fn fetch_random_bible_verse_json() -> String { let client = get_client(); let rt = get_runtime(); @@ -238,7 +308,7 @@ pub fn fetch_livestream_archive_json() -> String { let rt = get_runtime(); match rt.block_on(client.get_livestreams()) { - Ok(streams) => serde_json::to_string(&streams).unwrap_or_else(|_| "[]".to_string()), + Ok(streams) => format_sermon_items_with_dates(streams), Err(_) => "[]".to_string(), } } @@ -263,6 +333,30 @@ pub fn validate_contact_form_json(form_json: String) -> String { } } +pub fn validate_event_form_json(event_json: String) -> String { + match serde_json::from_str::(&event_json) { + Ok(event_data) => { + let result = validate_event_form(&event_data); + serde_json::to_string(&result).unwrap_or_else(|_| r#"{"is_valid": false, "errors": ["JSON serialization failed"]}"#.to_string()) + }, + Err(e) => { + let result = ValidationResult::invalid(vec![format!("Invalid JSON format: {}", e)]); + serde_json::to_string(&result).unwrap_or_else(|_| r#"{"is_valid": false, "errors": ["JSON parsing failed"]}"#.to_string()) + } + } +} + +pub fn validate_event_field_json(field_name: String, value: String, event_json: Option) -> String { + let event_data = if let Some(json) = event_json { + serde_json::from_str::(&json).ok() + } else { + None + }; + + let result = validate_event_field(&field_name, &value, event_data.as_ref()); + serde_json::to_string(&result).unwrap_or_else(|_| r#"{"is_valid": false, "errors": ["JSON serialization failed"]}"#.to_string()) +} + pub fn submit_event_json( title: String, description: String, diff --git a/src/models/event.rs b/src/models/event.rs index 1137ded..dfebbea 100644 --- a/src/models/event.rs +++ b/src/models/event.rs @@ -262,9 +262,14 @@ impl Event { } pub fn formatted_start_time(&self) -> String { - // Convert UTC to user's local timezone automatically - let local_time = self.start_time.with_timezone(&chrono::Local); - local_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string() + // Return time range (start - end time) for frontend compatibility + let start_local = self.start_time.with_timezone(&chrono::Local); + let end_local = self.end_time.with_timezone(&chrono::Local); + + let start_formatted = start_local.format("%I:%M %p").to_string().trim_start_matches('0').to_string(); + let end_formatted = end_local.format("%I:%M %p").to_string().trim_start_matches('0').to_string(); + + format!("{} - {}", start_formatted, end_formatted) } pub fn formatted_end_time(&self) -> String {