
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.
378 lines
19 KiB
Rust
378 lines
19 KiB
Rust
use axum::{extract::Path, response::Html};
|
|
use crate::services::ApiService;
|
|
use crate::layout::layout;
|
|
use chrono::NaiveDate;
|
|
|
|
pub async fn bulletins_handler() -> Html<String> {
|
|
let api_service = ApiService::new();
|
|
|
|
match api_service.get_bulletins().await {
|
|
Ok(bulletins) => {
|
|
let content = format!(r#"
|
|
<!-- Bulletins 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;">Church Bulletins</h1>
|
|
<p class="section-subtitle" style="color: rgba(255,255,255,0.9);">Download our weekly bulletins to stay informed about church activities and worship services</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Bulletins Grid -->
|
|
<section class="section-2025">
|
|
<div class="container-2025">
|
|
<div class="cards-grid">
|
|
{}
|
|
</div>
|
|
|
|
{}
|
|
</div>
|
|
</section>
|
|
"#,
|
|
// Bulletins grid HTML
|
|
bulletins.iter().enumerate().map(|(index, bulletin)| {
|
|
let formatted_date = if let Ok(parsed_date) = NaiveDate::parse_from_str(&bulletin.date, "%Y-%m-%d") {
|
|
parsed_date.format("%A, %B %d, %Y").to_string()
|
|
} else {
|
|
bulletin.date.clone()
|
|
};
|
|
|
|
format!(r#"
|
|
<a href="/bulletins/{}" style="text-decoration: none; color: inherit; display: block;">
|
|
<div class="card-2025 scroll-reveal stagger-{}" style="cursor: pointer;">
|
|
<div class="card-icon-2025">
|
|
<i class="fas fa-file-alt"></i>
|
|
</div>
|
|
<h3 class="card-title">{}</h3>
|
|
<p style="color: var(--medium-gray); margin-bottom: 1.5rem;">
|
|
<i class="fas fa-calendar" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
|
|
{}
|
|
</p>
|
|
|
|
{}
|
|
|
|
<div class="btn-2025 btn-outline" style="display: inline-flex; align-items: center; gap: 0.5rem; margin-top: auto; background: white; color: var(--deep-navy); border: 2px solid var(--deep-navy);">
|
|
View Details <i class="fas fa-arrow-right"></i>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
"#,
|
|
bulletin.id,
|
|
(index % 3) + 1,
|
|
bulletin.title,
|
|
formatted_date,
|
|
// Scripture reading preview
|
|
if let Some(ref scripture) = bulletin.scripture_reading {
|
|
if !scripture.is_empty() {
|
|
let preview = if scripture.len() > 150 {
|
|
format!("{}...", &scripture[..150])
|
|
} else {
|
|
scripture.clone()
|
|
};
|
|
format!(r#"
|
|
<div style="background: var(--soft-gray); padding: 1.5rem; border-radius: 12px; margin: 1.5rem 0; border-left: 4px solid var(--soft-gold); font-style: italic; color: var(--deep-navy);">
|
|
<strong style="color: var(--deep-navy);">Scripture Reading:</strong><br>
|
|
{}
|
|
</div>
|
|
"#, preview)
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
}
|
|
)
|
|
}).collect::<Vec<_>>().join(""),
|
|
|
|
// No bulletins message
|
|
if bulletins.is_empty() {
|
|
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-file-alt"></i>
|
|
</div>
|
|
<h3 class="card-title">No Bulletins Available</h3>
|
|
<p class="card-text">No bulletins available at this time. Please check back later.</p>
|
|
</div>
|
|
"#
|
|
} else {
|
|
""
|
|
}
|
|
);
|
|
|
|
Html(layout(&content, "Bulletins"))
|
|
},
|
|
Err(_) => {
|
|
let content = r#"
|
|
<section class="section-2025">
|
|
<div class="container-2025">
|
|
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
|
|
<h1>Bulletins</h1>
|
|
<p>Unable to load bulletins. Please try again later.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
"#;
|
|
Html(layout(content, "Bulletins"))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn bulletin_detail_handler(Path(id): Path<String>) -> Html<String> {
|
|
let api_service = ApiService::new();
|
|
|
|
match api_service.get_bulletin(&id).await {
|
|
Ok(Some(bulletin)) => {
|
|
let formatted_date = if let Ok(parsed_date) = NaiveDate::parse_from_str(&bulletin.date, "%Y-%m-%d") {
|
|
parsed_date.format("%A, %B %d, %Y").to_string()
|
|
} else {
|
|
bulletin.date.clone()
|
|
};
|
|
|
|
let content = format!(r#"
|
|
<!-- Bulletin Detail Hero -->
|
|
<section class="section-2025" style="background: var(--gradient-primary); color: white; padding: 6rem 0 3rem 0;">
|
|
<div class="container-2025">
|
|
<a href="/bulletins" 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 Bulletins
|
|
</a>
|
|
<h1 class="section-title serif" style="color: white; font-size: clamp(2.5rem, 5vw, 4rem); margin-bottom: 1rem;">{}</h1>
|
|
<p style="color: rgba(255,255,255,0.9); font-size: 1.2rem;">
|
|
<i class="fas fa-calendar-alt" style="color: var(--soft-gold); margin-right: 0.5rem;"></i>
|
|
{}
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
{}
|
|
|
|
{}
|
|
|
|
{}
|
|
|
|
{}
|
|
"#,
|
|
bulletin.title,
|
|
formatted_date,
|
|
|
|
// Scripture Reading Section
|
|
if let Some(ref scripture) = bulletin.scripture_reading {
|
|
if !scripture.is_empty() {
|
|
format!(r#"
|
|
<section class="section-2025">
|
|
<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">Scripture Reading</h3>
|
|
<div style="background: var(--soft-gray); padding: 2rem; border-radius: 16px; margin: 2rem 0; border-left: 4px solid var(--soft-gold); font-style: italic; color: var(--deep-navy); line-height: 1.6;">
|
|
{}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
"#, scripture)
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
},
|
|
|
|
// Service Programs Section
|
|
if bulletin.sabbath_school.is_some() || bulletin.divine_worship.is_some() {
|
|
let has_both = bulletin.sabbath_school.is_some() && bulletin.divine_worship.is_some();
|
|
format!(r#"
|
|
<section class="section-2025" style="background: var(--soft-gray);">
|
|
<div class="container-2025">
|
|
<div class="section-header scroll-reveal">
|
|
<h2 class="section-title">Service Programs</h2>
|
|
<p class="section-subtitle">Order of service for worship and fellowship</p>
|
|
</div>
|
|
|
|
{}
|
|
</div>
|
|
</section>
|
|
"#,
|
|
if has_both {
|
|
// Both programs - adaptive grid
|
|
format!(r#"
|
|
<div style="display: grid; grid-template-columns: 1fr 1.5fr; gap: 3rem; margin-top: 3rem; align-items: start;">
|
|
{}
|
|
{}
|
|
</div>
|
|
"#,
|
|
// Sabbath School - smaller card
|
|
if let Some(ref ss) = bulletin.sabbath_school {
|
|
format!(r#"
|
|
<div class="scroll-reveal" style="background: white; border-radius: 24px; padding: 3rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.2); height: fit-content;">
|
|
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2.5rem;">
|
|
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, var(--soft-gold), #ffd700); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(255, 215, 120, 0.3);">
|
|
<i class="fas fa-book" style="color: var(--deep-navy); font-size: 1.5rem;"></i>
|
|
</div>
|
|
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.8rem; margin: 0; font-weight: 600;">Sabbath School</h3>
|
|
</div>
|
|
|
|
<div style="color: var(--deep-navy); line-height: 2; font-size: 1.05rem;">
|
|
<div style="white-space: pre-wrap; font-family: inherit; margin: 0; line-height: inherit; font-size: inherit; color: inherit;">{}</div>
|
|
</div>
|
|
</div>
|
|
"#, ss)
|
|
} else {
|
|
String::new()
|
|
},
|
|
// Divine Worship - larger card
|
|
if let Some(ref dw) = bulletin.divine_worship {
|
|
format!(r#"
|
|
<div class="scroll-reveal" style="background: white; border-radius: 24px; padding: 3.5rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.2);">
|
|
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 3rem;">
|
|
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, var(--deep-navy), #2c4584); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(25, 35, 66, 0.3);">
|
|
<i class="fas fa-praying-hands" style="color: white; font-size: 1.5rem;"></i>
|
|
</div>
|
|
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 1.8rem; margin: 0; font-weight: 600;">Divine Worship</h3>
|
|
</div>
|
|
|
|
<div style="color: var(--deep-navy); line-height: 2.2; font-size: 1.05rem; letter-spacing: 0.3px; overflow: visible; word-wrap: break-word;">
|
|
<div style="white-space: pre-wrap; font-family: inherit; margin: 0; line-height: inherit; font-size: inherit; color: inherit; letter-spacing: inherit; overflow: visible; word-wrap: break-word;">{}</div>
|
|
</div>
|
|
</div>
|
|
"#, dw)
|
|
} else {
|
|
String::new()
|
|
}
|
|
)
|
|
} else {
|
|
// Single program or responsive fallback
|
|
format!(r#"
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 3rem; margin-top: 3rem;">
|
|
{}
|
|
{}
|
|
</div>
|
|
"#,
|
|
if let Some(ref ss) = bulletin.sabbath_school {
|
|
format!(r#"
|
|
<div class="scroll-reveal" style="background: white; border-radius: 24px; padding: 3rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.2);">
|
|
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2.5rem;">
|
|
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, var(--soft-gold), #ffd700); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(255, 215, 120, 0.3);">
|
|
<i class="fas fa-book" style="color: var(--deep-navy); font-size: 1.5rem;"></i>
|
|
</div>
|
|
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2rem; margin: 0; font-weight: 600;">Sabbath School Program</h3>
|
|
</div>
|
|
|
|
<div style="color: var(--deep-navy); line-height: 2; font-size: 1.05rem;">
|
|
<div style="white-space: pre-wrap; font-family: inherit; margin: 0; line-height: inherit; font-size: inherit; color: inherit;">{}</div>
|
|
</div>
|
|
</div>
|
|
"#, ss)
|
|
} else {
|
|
String::new()
|
|
},
|
|
if let Some(ref dw) = bulletin.divine_worship {
|
|
format!(r#"
|
|
<div class="scroll-reveal" style="background: white; border-radius: 24px; padding: 3.5rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.2);">
|
|
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 3rem;">
|
|
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, var(--deep-navy), #2c4584); border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(25, 35, 66, 0.3);">
|
|
<i class="fas fa-praying-hands" style="color: white; font-size: 1.5rem;"></i>
|
|
</div>
|
|
<h3 style="color: var(--deep-navy); font-family: 'Playfair Display', serif; font-size: 2rem; margin: 0; font-weight: 600;">Divine Worship Program</h3>
|
|
</div>
|
|
|
|
<div style="color: var(--deep-navy); line-height: 2.2; font-size: 1.05rem; letter-spacing: 0.3px; overflow: visible; word-wrap: break-word;">
|
|
<div style="white-space: pre-wrap; font-family: inherit; margin: 0; line-height: inherit; font-size: inherit; color: inherit; letter-spacing: inherit; overflow: visible; word-wrap: break-word;">{}</div>
|
|
</div>
|
|
</div>
|
|
"#, dw)
|
|
} else {
|
|
String::new()
|
|
}
|
|
)
|
|
})
|
|
} else {
|
|
String::new()
|
|
},
|
|
|
|
// Sunset Information Section
|
|
if let Some(ref sunset) = bulletin.sunset {
|
|
if !sunset.is_empty() {
|
|
format!(r#"
|
|
<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-sun"></i>
|
|
</div>
|
|
<h3 class="card-title">Sabbath Information</h3>
|
|
<p style="color: var(--medium-gray); font-style: italic; font-size: 1.1rem;">{}</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
"#, sunset)
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
},
|
|
|
|
// PDF Download Section
|
|
if let Some(ref pdf_path) = bulletin.pdf_path {
|
|
if !pdf_path.is_empty() {
|
|
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: 600px; margin: 0 auto;">
|
|
<div class="card-icon-2025" style="margin: 0 auto 2rem;">
|
|
<i class="fas fa-file-pdf"></i>
|
|
</div>
|
|
<h3 class="card-title">Download Full Bulletin</h3>
|
|
<p class="card-text">Get the complete bulletin with all details and information.</p>
|
|
<a href="{}" target="_blank" class="btn-2025 btn-primary" style="margin-top: 1rem;">
|
|
<i class="fas fa-download"></i>
|
|
Download PDF
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
"#, pdf_path)
|
|
} else {
|
|
String::new()
|
|
}
|
|
} else {
|
|
String::new()
|
|
}
|
|
);
|
|
|
|
Html(layout(&content, &bulletin.title))
|
|
},
|
|
Ok(None) => {
|
|
let content = r#"
|
|
<section class="section-2025">
|
|
<div class="container-2025">
|
|
<div class="card-2025 scroll-reveal" style="text-align: center; padding: 3rem;">
|
|
<h1>Bulletin Not Found</h1>
|
|
<p>The requested bulletin could not be found.</p>
|
|
<a href="/bulletins" class="btn-2025 btn-primary">← Back to Bulletins</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
"#;
|
|
Html(layout(content, "Bulletin Not Found"))
|
|
},
|
|
Err(_) => {
|
|
let content = 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>Unable to load bulletin. Please try again later.</p>
|
|
<a href="/bulletins" class="btn-2025 btn-primary">← Back to Bulletins</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
"#;
|
|
Html(layout(content, "Error"))
|
|
}
|
|
}
|
|
} |