church-api/church-website-axum/src/handlers/sermons.rs
Benjamin Slingo 0c06e159bb Initial commit: Church API Rust implementation
Complete church management system with bulletin management, media processing, live streaming integration, and web interface. Includes authentication, email notifications, database migrations, and comprehensive test suite.
2025-08-19 20:56:41 -04:00

757 lines
39 KiB
Rust

use axum::{extract::{Path, Query}, response::Html};
use crate::layout::layout;
use crate::services::{ApiService, parse_sermon_title, format_duration, format_date};
use serde::Deserialize;
use std::collections::HashMap;
use chrono::Datelike;
#[derive(Deserialize)]
pub struct ArchiveQuery {
collection: Option<String>,
}
pub async fn sermons_handler() -> Html<String> {
let api_service = ApiService::new();
match api_service.get_jellyfin_libraries().await {
Ok(libraries) => {
if libraries.is_empty() {
return render_no_sermons_page();
}
let mut collection_data = std::collections::HashMap::new();
for library in &libraries {
match api_service.get_jellyfin_sermons(Some(&library.id), Some(6)).await {
Ok(mut sermons) => {
// Sort sermons by date
sermons.sort_by(|a, b| {
let get_valid_date = |sermon: &crate::models::JellyfinItem| {
let parsed = parse_sermon_title(&sermon.name);
if let Some(ref date_str) = parsed.date_from_title {
if let Ok(date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
return date;
}
}
if let Some(ref premiere_date) = sermon.premiere_date {
if let Ok(date) = chrono::NaiveDate::parse_from_str(&premiere_date.split('T').next().unwrap_or(""), "%Y-%m-%d") {
return date;
}
}
chrono::Utc::now().naive_utc().date()
};
let date_a = get_valid_date(a);
let date_b = get_valid_date(b);
date_b.cmp(&date_a)
});
collection_data.insert(library.name.clone(), sermons);
},
Err(_) => {
collection_data.insert(library.name.clone(), vec![]);
}
}
}
Html(layout(&render_sermons_content(collection_data), "Latest Sermons & Live Streams"))
},
Err(_) => render_no_sermons_page()
}
}
fn render_no_sermons_page() -> Html<String> {
let content = r#"
<!-- Sermons Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 4rem 0;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Latest Sermons & Live Streams</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Listen to our most recent inspiring messages from God's Word</p>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-microphone-alt"></i>
</div>
<h3 class="card-title">Sermons Coming Soon</h3>
<p class="card-text">Sermons are currently being prepared for online streaming.</p>
<p class="card-text">Please check back later or contact us for more information.</p>
<p style="color: var(--medium-gray); font-style: italic; margin-top: 1rem;"><em>Note: Make sure Jellyfin server credentials are configured properly.</em></p>
</div>
</div>
</section>
"#;
Html(layout(content, "Sermons"))
}
fn render_sermons_content(collection_data: std::collections::HashMap<String, Vec<crate::models::JellyfinItem>>) -> String {
format!(r#"
<!-- Sermons Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 4rem 0;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h1 class="section-title serif" style="color: white;">Latest Sermons & Live Streams</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Listen to our most recent inspiring messages from God's Word. These are the latest sermons and live stream recordings for your spiritual growth.</p>
<div style="text-align: center; margin-top: 2rem;">
<a href="/sermons/archive" class="btn-2025 btn-outline" style="color: white; border-color: rgba(255,255,255,0.5); background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); font-size: 1.1rem; padding: 1rem 2rem;">
<i class="fas fa-archive" style="margin-right: 0.5rem;"></i>
Browse Complete Archive
</a>
</div>
</div>
</div>
</section>
{}
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title">About Our Sermons</h2>
<p class="section-subtitle">Our sermons focus on the Three Angels' Messages and the teachings of Jesus Christ. Each message is designed to strengthen your faith and deepen your understanding of God's Word.</p>
</div>
<div class="cards-grid">
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-calendar-week"></i>
</div>
<h3 class="card-title">Sabbath Sermons</h3>
<p class="card-text">Weekly messages during Divine Worship</p>
</div>
<div class="card-2025 scroll-reveal stagger-2">
<div class="card-icon-2025">
<i class="fas fa-scroll"></i>
</div>
<h3 class="card-title">Prophecy Studies</h3>
<p class="card-text">Deep dives into Biblical prophecy</p>
</div>
<div class="card-2025 scroll-reveal stagger-3">
<div class="card-icon-2025">
<i class="fas fa-heart"></i>
</div>
<h3 class="card-title">Practical Christianity</h3>
<p class="card-text">Applying Bible principles to daily life</p>
</div>
<div class="card-2025 scroll-reveal stagger-1">
<div class="card-icon-2025">
<i class="fas fa-star"></i>
</div>
<h3 class="card-title">Special Events</h3>
<p class="card-text">Revival meetings and guest speakers</p>
</div>
</div>
</div>
</section>
"#,
collection_data.iter().enumerate().map(|(collection_index, (collection_name, sermons))| {
format!(r#"
<section class="section-2025" style="{}">
<div class="container-2025">
<div class="section-header scroll-reveal">
<h2 class="section-title" style="display: flex; align-items: center; gap: 1rem;">
<i class="fas fa-{}" style="color: var(--soft-gold);"></i>
{}
</h2>
<p class="section-subtitle">
{}
</p>
<div style="text-align: center; margin-top: 2rem;">
<a href="/sermons/archive?collection={}" class="btn-2025 btn-outline" style="color: var(--deep-navy); border-color: var(--deep-navy);">
<i class="fas fa-archive" style="margin-right: 0.5rem;"></i>
View All {}
</a>
</div>
</div>
{}
</div>
</section>
"#,
if collection_index > 0 { "background: var(--soft-gray);" } else { "" },
if collection_name == "LiveStreams" { "broadcast-tower" } else { "church" },
if collection_name == "LiveStreams" { "Live Stream Recordings" } else { "Sabbath Sermons" },
if collection_name == "LiveStreams" {
"Recorded live streams from our worship services and special events"
} else {
"Messages from our regular Sabbath worship services"
},
collection_name,
if collection_name == "LiveStreams" { "Live Streams" } else { "Sermons" },
if sermons.is_empty() {
format!(r#"
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 2rem;">
<h4 style="color: var(--deep-navy); margin-bottom: 1rem;">No {} Available</h4>
<p style="color: var(--medium-gray);">Check back later for new content in this collection.</p>
</div>
"#, collection_name)
} else {
format!(r#"
<div class="cards-grid">
{}
</div>
"#, sermons.iter().enumerate().map(|(index, sermon)| {
let parsed = parse_sermon_title(&sermon.name);
format!(r#"
<div class="card-2025 scroll-reveal stagger-{}">
<div class="card-icon-2025">
<i class="fas fa-{}"></i>
</div>
<h3 class="card-title">{}</h3>
{}
{}
{}
<div style="display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap;">
{}
<span style="background: var(--soft-gold); color: var(--deep-navy); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.9rem; font-weight: 500;">
<i class="fas fa-{}" style="margin-right: 0.25rem;"></i>
{}
</span>
</div>
<a href="/sermons/{}" class="btn-2025 btn-primary" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-{}"></i>
{} {}
</a>
</div>
"#,
(index % 3) + 1,
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "music" } else { "video" },
parsed.title,
if let Some(ref speaker) = parsed.speaker {
format!(r#"
<p style="color: var(--soft-gold); margin-bottom: 1rem; font-weight: 500;">
<i class="fas fa-user" style="margin-right: 0.5rem;"></i>{}
</p>
"#, speaker)
} else {
String::new()
},
if let Some(ref premiere_date) = sermon.premiere_date {
format!(r#"
<p style="color: var(--medium-gray); margin-bottom: 1rem;">
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{}
</p>
"#, format_date(premiere_date))
} else if let Some(ref date_from_title) = parsed.date_from_title {
format!(r#"
<p style="color: var(--medium-gray); margin-bottom: 1rem;">
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
{}
</p>
"#, date_from_title)
} else {
String::new()
},
if let Some(ref overview) = sermon.overview {
let preview = if overview.len() > 150 {
format!("{}...", &overview[..150])
} else {
overview.clone()
};
format!(r#"
<p style="color: var(--medium-gray); margin-bottom: 1.5rem; line-height: 1.5;">
{}
</p>
"#, preview)
} else {
String::new()
},
if let Some(ticks) = sermon.run_time_ticks {
format!(r#"
<span style="background: var(--soft-gray); color: var(--deep-navy); padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.9rem; font-weight: 500;">
<i class="fas fa-clock" style="margin-right: 0.25rem;"></i>
{}
</span>
"#, format_duration(ticks))
} else {
String::new()
},
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "music" } else { "video" },
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "Audio" } else { "Video" },
sermon.id,
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "play" } else { "play-circle" },
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "Listen" } else { "Watch" },
if collection_name == "LiveStreams" { "Recording" } else { "Sermon" }
)
}).collect::<Vec<_>>().join(""))
})
}).collect::<Vec<_>>().join(""))
}
pub async fn sermon_detail_handler(Path(id): Path<String>) -> Html<String> {
let api_service = ApiService::new();
match api_service.get_jellyfin_sermon(&id).await {
Ok(Some(sermon)) => {
match api_service.authenticate_jellyfin().await {
Ok(Some((token, _))) => {
let parsed = parse_sermon_title(&sermon.name);
let stream_url = api_service.get_jellyfin_stream_url(&sermon.id, &token);
let content = format!(r#"
<!-- Sermon Detail Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 4rem 0 3rem 0;">
<div class="container-2025">
<a href="/sermons" style="color: rgba(255,255,255,0.8); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-weight: 500; margin-top: 1rem;">
<i class="fas fa-arrow-left"></i> Back to Sermons
</a>
<h1 class="section-title serif" style="color: white; font-size: clamp(2.5rem, 5vw, 4rem); margin-bottom: 1rem;">{}</h1>
{}
<div style="display: flex; gap: 2rem; flex-wrap: wrap; color: rgba(255,255,255,0.9);">
{}
{}
</div>
</div>
</section>
<!-- Sermon Player -->
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 900px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-{}"></i>
</div>
<h3 class="card-title">{}</h3>
{}
</div>
</div>
</section>
{}
<!-- Share Section -->
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 600px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-share-alt"></i>
</div>
<h3 class="card-title">Share This Sermon</h3>
<p class="card-text">Invite others to listen to this inspiring message:</p>
<button onclick="copyToClipboard(window.location.href)" class="btn-2025 btn-primary" style="margin-top: 1rem;">
<i class="fas fa-copy"></i>
Copy Link
</button>
</div>
</div>
</section>
<script>
function copyToClipboard(text) {{
navigator.clipboard.writeText(text).then(function() {{
alert('Link copied to clipboard!');
}});
}}
</script>
"#,
parsed.title,
if let Some(ref speaker) = parsed.speaker {
format!(r#"
<p style="color: var(--soft-gold); font-size: 1.3rem; margin-bottom: 1rem; font-weight: 500;">
<i class="fas fa-user" style="margin-right: 0.5rem;"></i>Speaker: {}
</p>
"#, speaker)
} else {
String::new()
},
if let Some(ref premiere_date) = sermon.premiere_date {
format!(r#"
<p style="display: flex; align-items: center; gap: 0.5rem; font-size: 1.1rem;">
<i class="fas fa-calendar-alt" style="color: var(--soft-gold);"></i>
{}
</p>
"#, format_date(premiere_date))
} else if let Some(ref date_from_title) = parsed.date_from_title {
format!(r#"
<p style="display: flex; align-items: center; gap: 0.5rem; font-size: 1.1rem;">
<i class="fas fa-calendar-alt" style="color: var(--soft-gold);"></i>
{}
</p>
"#, date_from_title)
} else {
String::new()
},
if let Some(ticks) = sermon.run_time_ticks {
format!(r#"
<p style="display: flex; align-items: center; gap: 0.5rem; font-size: 1.1rem;">
<i class="fas fa-clock" style="color: var(--soft-gold);"></i>
{}
</p>
"#, format_duration(ticks))
} else {
String::new()
},
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "music" } else { "video" },
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" { "Audio Sermon" } else { "Video Sermon" },
if sermon.media_type.as_ref().unwrap_or(&"Audio".to_string()) == "Audio" {
format!(r#"
<audio src="{}" controls preload="auto" style="width: 100%; max-width: 600px; margin: 2rem 0; border-radius: 8px;" crossorigin="anonymous">
Your browser does not support the audio element.
</audio>
"#, stream_url)
} else {
format!(r#"
<video src="{}" controls playsinline webkit-playsinline preload="auto" x-webkit-airplay="allow" crossorigin="anonymous" style="width: 100%; max-width: 800px; margin: 2rem 0; border-radius: 8px;">
Your browser does not support the video element.
</video>
"#, stream_url)
},
if let Some(ref overview) = sermon.overview {
format!(r#"
<section class="section-2025" style="background: var(--soft-gray);">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; max-width: 800px; margin: 0 auto;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-book-open"></i>
</div>
<h3 class="card-title">Description</h3>
<p style="color: var(--medium-gray); line-height: 1.6; font-size: 1.1rem;">{}</p>
</div>
</div>
</section>
"#, overview)
} else {
String::new()
}
);
Html(layout(&content, &parsed.title))
},
_ => render_sermon_error("Unable to access sermon content. Please try again later.")
}
},
Ok(None) => render_sermon_error("The requested sermon could not be found."),
Err(_) => render_sermon_error("Unable to load sermon. Please try again later.")
}
}
fn render_sermon_error(message: &str) -> Html<String> {
let content = format!(r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<h1>Error</h1>
<p>{}</p>
<a href="/sermons" class="btn-2025 btn-primary">← Back to Sermons</a>
</div>
</div>
</section>
"#, message);
Html(layout(&content, "Error"))
}
pub async fn sermons_archive_handler(Query(params): Query<ArchiveQuery>) -> Html<String> {
let api_service = ApiService::new();
match api_service.get_jellyfin_libraries().await {
Ok(libraries) => {
// If a specific collection is selected, show only that one
if let Some(selected_collection) = params.collection {
if let Some(library) = libraries.iter().find(|lib| lib.name == selected_collection) {
match api_service.get_jellyfin_sermons(Some(&library.id), None).await {
Ok(sermons) => {
let organized = organize_sermons_by_year_month(&sermons);
let mut years: Vec<String> = organized.keys().cloned().collect();
years.sort_by(|a, b| b.parse::<i32>().unwrap_or(0).cmp(&a.parse::<i32>().unwrap_or(0)));
return render_archive_page(&organized, &years, &selected_collection, &libraries);
}
Err(_) => return render_error_page()
}
} else {
return render_error_page();
}
}
// Default to showing Sermons collection (skip multi-collection view)
let default_collection = libraries.iter()
.find(|lib| lib.name == "Sermons")
.or_else(|| libraries.first());
if let Some(library) = default_collection {
match api_service.get_jellyfin_sermons(Some(&library.id), None).await {
Ok(sermons) => {
let organized = organize_sermons_by_year_month(&sermons);
let mut years: Vec<String> = organized.keys().cloned().collect();
years.sort_by(|a, b| b.parse::<i32>().unwrap_or(0).cmp(&a.parse::<i32>().unwrap_or(0)));
return render_archive_page(&organized, &years, &library.name, &libraries);
}
Err(_) => return render_error_page()
}
} else {
return render_error_page();
}
}
Err(_) => return render_error_page()
}
}
fn organize_sermons_by_year_month(sermons: &[crate::models::JellyfinItem]) -> HashMap<String, HashMap<String, Vec<&crate::models::JellyfinItem>>> {
let mut organized: HashMap<String, HashMap<String, Vec<&crate::models::JellyfinItem>>> = HashMap::new();
for sermon in sermons {
let parsed = parse_sermon_title(&sermon.name);
let date = if let Some(ref date_str) = parsed.date_from_title {
// Try parsing the date from title using multiple formats
chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
.or_else(|_| chrono::NaiveDate::parse_from_str(date_str, "%m/%d/%Y"))
.or_else(|_| chrono::NaiveDate::parse_from_str(date_str, "%m-%d-%Y"))
.unwrap_or_else(|_| {
// If date parsing fails, use premiere date or created date as fallback
if let Some(ref premiere_date) = sermon.premiere_date {
chrono::NaiveDate::parse_from_str(&premiere_date[..10], "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date())
} else if let Some(ref created_date) = sermon.date_created {
chrono::NaiveDate::parse_from_str(&created_date[..10], "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date())
} else {
chrono::Utc::now().naive_utc().date()
}
})
} else if let Some(ref premiere_date) = sermon.premiere_date {
chrono::NaiveDate::parse_from_str(&premiere_date[..10], "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date())
} else if let Some(ref created_date) = sermon.date_created {
chrono::NaiveDate::parse_from_str(&created_date[..10], "%Y-%m-%d")
.unwrap_or_else(|_| chrono::Utc::now().naive_utc().date())
} else {
chrono::Utc::now().naive_utc().date()
};
let year = date.year().to_string();
let month = date.format("%B").to_string();
organized
.entry(year)
.or_insert_with(HashMap::new)
.entry(month)
.or_insert_with(Vec::new)
.push(sermon);
}
organized
}
fn render_archive_page(
organized: &HashMap<String, HashMap<String, Vec<&crate::models::JellyfinItem>>>,
years: &[String],
selected_collection: &str,
libraries: &[crate::models::JellyfinLibrary]
) -> Html<String> {
let collection_display_name = if selected_collection == "LiveStreams" {
"Live Stream Recordings"
} else {
"Sabbath Sermons"
};
let content = format!(r#"
<!-- Archive Hero -->
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 4rem 0;">
<div class="container-2025">
<div class="section-header scroll-reveal">
<a href="/sermons" style="color: rgba(255,255,255,0.8); text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; margin-bottom: 2rem; font-weight: 500;">
<i class="fas fa-arrow-left"></i> Back to Latest Sermons
</a>
<h1 class="section-title serif" style="color: white;">{} Archive</h1>
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Browse the complete collection organized by year and month.</p>
<!-- Collection Navigation -->
<div style="display: flex; gap: 1rem; margin-top: 2rem; flex-wrap: wrap; justify-content: center;">
{}
</div>
</div>
</div>
</section>
<section class="section-2025">
<div class="container-2025">
{}
</div>
</section>
<script>
function toggleYear(yearId) {{
const content = document.getElementById(yearId);
const chevron = document.getElementById('chevron-' + yearId);
if (content.style.display === 'none' || content.style.display === '') {{
content.style.display = 'block';
chevron.style.transform = 'rotate(180deg)';
}} else {{
content.style.display = 'none';
chevron.style.transform = 'rotate(0deg)';
}}
}}
function toggleMonth(monthId) {{
const content = document.getElementById(monthId);
const chevron = document.getElementById('chevron-' + monthId);
if (content.style.display === 'none' || content.style.display === '') {{
content.style.display = 'block';
chevron.style.transform = 'rotate(180deg)';
}} else {{
content.style.display = 'none';
chevron.style.transform = 'rotate(0deg)';
}}
}}
// Auto-expand current year
const currentYear = new Date().getFullYear();
const currentYearElement = document.getElementById('year-' + currentYear);
if (currentYearElement) {{
toggleYear('year-' + currentYear);
}}
</script>
"#,
collection_display_name,
libraries.iter().map(|lib| {
let display_name = if lib.name == "LiveStreams" { "Live Streams" } else { "Sermons" };
let icon = if lib.name == "LiveStreams" { "broadcast-tower" } else { "church" };
let active_class = if lib.name == selected_collection { " active" } else { "" };
format!(r#"<a href="/sermons/archive?collection={}" class="btn-2025{}" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-{}"></i> {}
</a>"#, lib.name, if active_class.is_empty() { " btn-outline" } else { " btn-primary" }, icon, display_name)
}).collect::<Vec<_>>().join(""),
if years.is_empty() {
format!(r#"<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-archive"></i>
</div>
<h3 class="card-title">No {} Found</h3>
<p class="card-text">This collection doesn't contain any items yet. Please check back later.</p>
</div>"#, collection_display_name)
} else {
years.iter().map(|year| {
let year_data = organized.get(year).unwrap();
let mut months: Vec<&String> = year_data.keys().collect();
months.sort_by(|a, b| {
let month_a = chrono::NaiveDate::parse_from_str(&format!("{} 1, 2020", a), "%B %d, %Y").unwrap().month();
let month_b = chrono::NaiveDate::parse_from_str(&format!("{} 1, 2020", b), "%B %d, %Y").unwrap().month();
month_b.cmp(&month_a)
});
let total_items: usize = year_data.values().map(|sermons| sermons.len()).sum();
format!(r#"
<div class="year-section scroll-reveal" style="margin-bottom: 3rem; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
<div class="year-header" style="background: var(--deep-navy); color: white; padding: 1.5rem 2rem; display: flex; align-items: center; justify-content: space-between; cursor: pointer;" onclick="toggleYear('year-{}')">
<h2 style="margin: 0; font-family: 'Playfair Display', serif; font-size: 2rem; color: white;">
<i class="fas fa-calendar-alt" style="margin-right: 1rem; color: var(--soft-gold);"></i>{}
</h2>
<div style="display: flex; align-items: center; gap: 1rem;">
<span style="background: var(--soft-gold); color: var(--deep-navy); padding: 0.5rem 1rem; border-radius: 20px; font-weight: 600;">{} items</span>
<i class="fas fa-chevron-down" id="chevron-year-{}" style="color: var(--soft-gold); font-size: 1.2rem; transition: transform 0.3s ease;"></i>
</div>
</div>
<div class="year-content" id="year-{}" style="background: white; border: 1px solid #e5e7eb; border-top: none; display: none;">
{}
</div>
</div>
"#, year, year, total_items, year, year,
months.iter().map(|month| {
let month_sermons = year_data.get(*month).unwrap();
let month_id = format!("{}-{}", year, month.replace(" ", ""));
format!(r#"
<div class="month-section" style="border-bottom: 1px solid #f3f4f6;">
<div class="month-header" style="background: var(--soft-gray); padding: 1.25rem 2rem; display: flex; align-items: center; justify-content: space-between; cursor: pointer;" onclick="toggleMonth('month-{}')">
<h3 style="margin: 0; color: var(--deep-navy); font-size: 1.25rem; font-weight: 600;">
<i class="fas fa-folder" style="margin-right: 0.75rem; color: var(--soft-gold);"></i>
{} {}
</h3>
<div style="display: flex; align-items: center; gap: 1rem;">
<span style="background: white; color: var(--deep-navy); padding: 0.25rem 0.75rem; border-radius: 12px; font-weight: 500;">{} item{}</span>
<i class="fas fa-chevron-down" id="chevron-month-{}" style="color: var(--deep-navy); transition: transform 0.3s ease;"></i>
</div>
</div>
<div class="month-content" id="month-{}" style="padding: 1.5rem 2rem; display: none;">
<div style="display: grid; gap: 1.5rem;">
{}
</div>
</div>
</div>
"#, month_id, month, year, month_sermons.len(), if month_sermons.len() == 1 { "" } else { "s" }, month_id, month_id,
month_sermons.iter().map(|sermon| {
let parsed = parse_sermon_title(&sermon.name);
let premiere_date = sermon.premiere_date.as_ref().map(|d| format_date(d)).unwrap_or_default();
let default_media_type = "Video".to_string();
let media_type = sermon.media_type.as_ref().unwrap_or(&default_media_type);
format!(r#"
<div style="display: flex; align-items: center; justify-content: space-between; padding: 1.25rem; background: var(--soft-gray); border-radius: 12px; transition: all 0.3s ease; cursor: pointer;" onclick="window.open('/sermons/{}', '_blank')" onmouseover="this.style.background='#f8fafc'; this.style.transform='translateY(-2px)'" onmouseout="this.style.background='var(--soft-gray)'; this.style.transform='translateY(0)'">
<div style="flex-grow: 1;">
<h4 style="margin: 0 0 0.5rem 0; color: var(--deep-navy); font-size: 1.1rem; font-weight: 600;">{}</h4>
<div style="display: flex; gap: 1.5rem; align-items: center; flex-wrap: wrap;">
{}
{}
<span style="background: var(--soft-gold); color: var(--deep-navy); padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.8rem; font-weight: 500;">
<i class="fas fa-{}"></i> {}
</span>
</div>
</div>
<div style="margin-left: 1rem;">
<i class="fas fa-play-circle" style="color: var(--deep-navy); font-size: 2rem;"></i>
</div>
</div>
"#, sermon.id, parsed.title,
if let Some(speaker) = parsed.speaker {
format!(r#"<span style="color: var(--medium-gray); display: flex; align-items: center; gap: 0.5rem;"><i class="fas fa-user" style="color: var(--soft-gold);"></i>{}</span>"#, speaker)
} else { String::new() },
if !premiere_date.is_empty() {
format!(r#"<span style="color: var(--medium-gray); display: flex; align-items: center; gap: 0.5rem;"><i class="fas fa-calendar" style="color: var(--soft-gold);"></i>{}</span>"#, premiere_date)
} else { String::new() },
if media_type == "Audio" { "music" } else { "video" },
media_type)
}).collect::<Vec<_>>().join(""))
}).collect::<Vec<_>>().join(""))
}).collect::<Vec<_>>().join("")
});
Html(layout(&content, &format!("{} Archive", collection_display_name)))
}
fn render_error_page() -> Html<String> {
let content = r#"
<section class="section-2025">
<div class="container-2025">
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h3 class="card-title">Archive Unavailable</h3>
<p class="card-text">Unable to load sermon archive at this time. Please check back later or contact us for assistance.</p>
<a href="/sermons" class="btn-2025 btn-primary" style="margin-top: 2rem;">← Back to Latest Sermons</a>
</div>
</div>
</section>
"#;
Html(layout(content, "Sermon Archive"))
}