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
This commit is contained in:
Benjamin Slingo 2025-08-30 15:59:37 -04:00
parent d83467939f
commit dbcbf9626f
2 changed files with 105 additions and 6 deletions

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
ChurchApiClient, ChurchCoreConfig, ChurchApiClient, ChurchCoreConfig,
models::{NewSchedule, ScheduleUpdate, NewBulletin, BulletinUpdate, NewEvent, EventUpdate}, models::{NewSchedule, ScheduleUpdate, NewBulletin, BulletinUpdate, NewEvent, EventUpdate},
utils::validation::{validate_event_form, validate_event_field, EventFormData, ValidationResult},
}; };
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use std::sync::OnceLock; use std::sync::OnceLock;
@ -141,7 +142,7 @@ pub fn fetch_events_json() -> String {
let client = get_client(); let client = get_client();
let rt = get_runtime(); 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) => { Ok(events) => {
// Format events with display formatting using existing Event methods // Format events with display formatting using existing Event methods
let formatted_events: Vec<_> = events.iter().map(|event| { 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<T>(items: Vec<T>) -> 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 { pub fn fetch_sermons_json() -> String {
let client = get_client(); let client = get_client();
let rt = get_runtime(); let rt = get_runtime();
match rt.block_on(client.get_recent_sermons(Some(20))) { 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(), 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 { pub fn fetch_config_json() -> String {
let client = get_client(); let client = get_client();
let rt = get_runtime(); 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 { pub fn fetch_random_bible_verse_json() -> String {
let client = get_client(); let client = get_client();
let rt = get_runtime(); let rt = get_runtime();
@ -238,7 +308,7 @@ pub fn fetch_livestream_archive_json() -> String {
let rt = get_runtime(); let rt = get_runtime();
match rt.block_on(client.get_livestreams()) { 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(), 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::<EventFormData>(&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>) -> String {
let event_data = if let Some(json) = event_json {
serde_json::from_str::<EventFormData>(&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( pub fn submit_event_json(
title: String, title: String,
description: String, description: String,

View file

@ -262,9 +262,14 @@ impl Event {
} }
pub fn formatted_start_time(&self) -> String { pub fn formatted_start_time(&self) -> String {
// Convert UTC to user's local timezone automatically // Return time range (start - end time) for frontend compatibility
let local_time = self.start_time.with_timezone(&chrono::Local); let start_local = self.start_time.with_timezone(&chrono::Local);
local_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string() 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 { pub fn formatted_end_time(&self) -> String {