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:
parent
d83467939f
commit
dbcbf9626f
100
src/api.rs
100
src/api.rs
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue