From 4cfd1bad843396cef70222c4b6159e82b9903873 Mon Sep 17 00:00:00 2001 From: Benjamin Slingo Date: Tue, 26 Aug 2025 18:09:31 -0400 Subject: [PATCH] Add PO Box support and centralize config via API - Add PO Box field to bulletin contact section from API config - Replace local config files with API-based config fetching - Add cover image upload support to bulletin-input tool - Remove dead config.rs and config.toml files - Unify contact info source to prevent config segregation --- bulletin-generator/src/main.rs | 28 +++++--- bulletin-generator/src/template_renderer.rs | 1 + .../templates/bulletin_template.html | 1 + bulletin-input/src/main.rs | 36 ++++++++-- bulletin-shared/src/config.rs | 24 ------- bulletin-shared/src/lib.rs | 2 - bulletin-shared/src/models.rs | 16 +++++ bulletin-shared/src/new_api.rs | 70 ++++++++++++++++++- shared/config.toml | 6 -- 9 files changed, 134 insertions(+), 50 deletions(-) delete mode 100644 bulletin-shared/src/config.rs delete mode 100644 shared/config.toml diff --git a/bulletin-generator/src/main.rs b/bulletin-generator/src/main.rs index 0ac11b3..ce2980e 100644 --- a/bulletin-generator/src/main.rs +++ b/bulletin-generator/src/main.rs @@ -4,7 +4,7 @@ mod template_renderer; use anyhow::{anyhow, Result}; use base64::prelude::*; -use bulletin_shared::{Config, NewApiClient}; +use bulletin_shared::NewApiClient; use chrono::{Datelike, Local, NaiveDate, Weekday}; use clap::Parser; use std::path::PathBuf; @@ -12,9 +12,6 @@ use std::path::PathBuf; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - #[arg(short, long, default_value = "shared/config.toml")] - config: PathBuf, - #[arg(short, long)] date: Option, @@ -56,8 +53,6 @@ async fn main() -> Result<()> { let args = Args::parse(); - let config = Config::from_file(&args.config)?; - let target_date = args.date.unwrap_or_else(|| get_upcoming_saturday(None)); println!("--- Starting bulletin generation process for date: {} ---", target_date); @@ -123,6 +118,18 @@ async fn main() -> Result<()> { eprintln!("Failed to fetch events: {}", e); Vec::new() }); + + println!("Fetching config from API..."); + let api_config = client.get_config().await.unwrap_or_else(|e| { + eprintln!("Failed to fetch config from API, using defaults: {}", e); + bulletin_shared::models::ApiConfig { + church_name: "Rockville-Tolland Seventh-Day Adventist Church".to_string(), + church_address: "9 Hartford Tpke Tolland CT 06084".to_string(), + po_box: "".to_string(), + contact_phone: "860-875-0450".to_string(), + contact_email: "admin@rockvilletollandsda.church".to_string(), + } + }); println!("Parsing Sabbath School text..."); let sabbath_school_items = html_parser::parse_sabbath_school(&bulletin.sabbath_school_section); @@ -138,16 +145,17 @@ async fn main() -> Result<()> { let context = template_renderer::TemplateContext { bulletin_date: target_date.format("%B %d, %Y").to_string(), bulletin_theme_title: html_parser::strip_html_tags(&bulletin.sermon_title), - church_name: config.church_name.clone(), + church_name: api_config.church_name.clone(), cover_image_path: cover_image_path.as_ref().map(|p| p.to_string_lossy().to_string()), sabbath_school_items, divine_worship_items, announcements, sunset_times: new_bulletin.sunset.unwrap_or_else(|| "TBA".to_string()), contact_info: template_renderer::ContactInfo { - phone: config.contact_phone.clone().unwrap_or_else(|| "860-875-0450".to_string()), - website: config.contact_website.clone().unwrap_or_else(|| "rockvilletollandsda.church".to_string()), - address: config.contact_address.clone().unwrap_or_else(|| "9 Hartford Tpke Tolland CT 06084".to_string()), + phone: api_config.contact_phone.clone(), + website: "rockvilletollandsda.church".to_string(), // Could be added to API config if needed + address: api_config.church_address.clone(), + po_box: if api_config.po_box.is_empty() { None } else { Some(api_config.po_box.clone()) }, }, }; diff --git a/bulletin-generator/src/template_renderer.rs b/bulletin-generator/src/template_renderer.rs index 5538701..0387c77 100644 --- a/bulletin-generator/src/template_renderer.rs +++ b/bulletin-generator/src/template_renderer.rs @@ -25,6 +25,7 @@ pub struct ContactInfo { pub phone: String, pub website: String, pub address: String, + pub po_box: Option, } #[derive(Debug, Serialize)] diff --git a/bulletin-generator/templates/bulletin_template.html b/bulletin-generator/templates/bulletin_template.html index 381abd2..c120891 100644 --- a/bulletin-generator/templates/bulletin_template.html +++ b/bulletin-generator/templates/bulletin_template.html @@ -102,6 +102,7 @@

Phone: {{ contact_info.phone | default(value='860-875-0450') }}

Website: {{ contact_info.website | default(value='rockvilletollandsda.church') }}

Address: {{ contact_info.address | default(value='9 Hartford Tpke Tolland CT 06084') }}

+ {% if contact_info.po_box %}

{{ contact_info.po_box }}

{% endif %}

Sunset Times

{{ sunset_times | default(value='Not available') }}

diff --git a/bulletin-input/src/main.rs b/bulletin-input/src/main.rs index 00b5942..a6adfef 100644 --- a/bulletin-input/src/main.rs +++ b/bulletin-input/src/main.rs @@ -1,18 +1,14 @@ mod sermon_parser; use anyhow::{anyhow, Result}; -use bulletin_shared::{Config, Bulletin, NewApiClient}; +use bulletin_shared::{Bulletin, NewApiClient}; use chrono::{Datelike, Local, NaiveDate, Weekday}; use clap::Parser; use dialoguer::{Input, theme::ColorfulTheme}; -use std::path::PathBuf; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - #[arg(short, long, default_value = "shared/config.toml")] - config: PathBuf, - #[arg(short, long)] date: Option, @@ -160,8 +156,6 @@ async fn main() -> Result<()> { let args = Args::parse(); - let _config = Config::from_file(&args.config)?; - let target_saturday = get_upcoming_saturday(args.date); println!("--- Creating bulletin for Saturday: {} ---", target_saturday); @@ -341,11 +335,39 @@ async fn main() -> Result<()> { updated: None, }; + println!("\n--- Optional Cover Image ---"); + println!("Enter cover image file path (press Enter to skip):"); + let mut image_path = String::new(); + std::io::stdin().read_line(&mut image_path)?; + let image_path = image_path.trim(); + + let image_file_path = if !image_path.is_empty() && std::path::Path::new(image_path).exists() { + Some(std::path::PathBuf::from(image_path)) + } else if !image_path.is_empty() { + println!("Warning: Image file '{}' not found, continuing without cover image", image_path); + None + } else { + None + }; + println!("\n--- Creating bulletin in new API ---"); let created_bulletin = client.create_bulletin_from_bulletin(&bulletin).await?; println!("\nSUCCESS! Created bulletin ID: {}", created_bulletin.id); + + // Upload cover image if provided + if let Some(image_path) = image_file_path { + println!("Uploading cover image..."); + match client.upload_cover_image(&created_bulletin.id, &image_path).await { + Ok(_) => println!("Cover image uploaded successfully!"), + Err(e) => { + eprintln!("Failed to upload cover image: {}", e); + println!("Bulletin created successfully, but cover image upload failed."); + } + } + } + println!("Done! Check your API - bulletin created successfully!"); Ok(()) diff --git a/bulletin-shared/src/config.rs b/bulletin-shared/src/config.rs deleted file mode 100644 index db610a3..0000000 --- a/bulletin-shared/src/config.rs +++ /dev/null @@ -1,24 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::path::Path; -use anyhow::Result; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Config { - pub church_name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub contact_phone: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub contact_website: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub contact_youtube: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub contact_address: Option, -} - -impl Config { - pub fn from_file>(path: P) -> Result { - let content = std::fs::read_to_string(path)?; - let config: Config = toml::from_str(&content)?; - Ok(config) - } -} \ No newline at end of file diff --git a/bulletin-shared/src/lib.rs b/bulletin-shared/src/lib.rs index d18aa42..8ca8659 100644 --- a/bulletin-shared/src/lib.rs +++ b/bulletin-shared/src/lib.rs @@ -1,7 +1,5 @@ -pub mod config; pub mod models; pub mod new_api; -pub use config::Config; pub use models::{Bulletin, Event, Personnel, NewApiBulletin, NewApiResponse, NewApiEvent, NewApiEventsResponse, ConferenceDataResponse, ConferenceData, ScheduleResponse, ScheduleData, PersonnelData, LoginRequest, LoginResponse, LoginData}; pub use new_api::NewApiClient; \ No newline at end of file diff --git a/bulletin-shared/src/models.rs b/bulletin-shared/src/models.rs index 5e952f4..5ebaef8 100644 --- a/bulletin-shared/src/models.rs +++ b/bulletin-shared/src/models.rs @@ -208,4 +208,20 @@ pub struct LoginResponse { #[derive(Debug, Deserialize)] pub struct LoginData { pub token: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ApiConfig { + pub church_name: String, + pub church_address: String, + pub po_box: String, + pub contact_phone: String, + pub contact_email: String, +} + +#[derive(Debug, Deserialize)] +pub struct ApiConfigResponse { + pub success: bool, + pub data: Option, + pub message: Option, } \ No newline at end of file diff --git a/bulletin-shared/src/new_api.rs b/bulletin-shared/src/new_api.rs index ccb029a..045abd8 100644 --- a/bulletin-shared/src/new_api.rs +++ b/bulletin-shared/src/new_api.rs @@ -1,4 +1,4 @@ -use crate::models::{NewApiResponse, NewApiBulletin, Bulletin, NewApiEventsResponse, NewApiEvent, Event, EventType, ConferenceDataResponse, ConferenceData, ScheduleResponse, PersonnelData, LoginRequest, LoginResponse}; +use crate::models::{NewApiResponse, NewApiBulletin, Bulletin, NewApiEventsResponse, NewApiEvent, Event, EventType, ConferenceDataResponse, ConferenceData, ScheduleResponse, PersonnelData, LoginRequest, LoginResponse, ApiConfig, ApiConfigResponse}; use anyhow::{anyhow, Result}; use reqwest::Client; use chrono::{NaiveDate, DateTime, Utc}; @@ -179,6 +179,52 @@ impl NewApiClient { Ok(response_text) } + pub async fn upload_cover_image(&self, bulletin_id: &str, image_path: &std::path::Path) -> Result { + let url = format!("{}/api/upload/bulletins/{}/cover", self.base_url, bulletin_id); + + let file = tokio::fs::read(image_path).await?; + let file_name = image_path.file_name() + .ok_or_else(|| anyhow!("Invalid file path"))? + .to_string_lossy() + .to_string(); + + // Determine MIME type based on file extension + let mime_type = match image_path.extension().and_then(|ext| ext.to_str()) { + Some("jpg") | Some("jpeg") => "image/jpeg", + Some("png") => "image/png", + Some("webp") => "image/webp", + Some("gif") => "image/gif", + _ => "image/jpeg", // default + }; + + let part = reqwest::multipart::Part::bytes(file) + .file_name(file_name) + .mime_str(mime_type)?; + + println!("Debug: Uploading cover image to bulletin_id: '{}'", bulletin_id); + println!("Debug: Upload URL: {}", url); + + let form = reqwest::multipart::Form::new() + .part("file", part); + + let mut request = self.client.post(&url).multipart(form); + + if let Some(ref token) = self.auth_token { + request = request.header("Authorization", format!("Bearer {}", token)); + } + + let response = request.send().await?; + + let status = response.status(); + let response_text = response.text().await?; + + if !status.is_success() { + return Err(anyhow!("Failed to upload cover image (HTTP {}): {}", status, response_text)); + } + + Ok(response_text) + } + pub async fn get_conference_data(&self, date: NaiveDate) -> Result { let date_str = date.format("%Y-%m-%d").to_string(); let url = format!("{}/api/conference-data?date={}", self.base_url, date_str); @@ -224,6 +270,28 @@ impl NewApiClient { Ok(api_response.data.personnel) } + + pub async fn get_config(&self) -> Result { + let url = format!("{}/api/config", self.base_url); + + let response = self.client + .get(&url) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(anyhow!("Failed to get config: {}", error_text)); + } + + let api_response: ApiConfigResponse = response.json().await?; + + if !api_response.success { + return Err(anyhow!("API returned error: {:?}", api_response.message)); + } + + api_response.data.ok_or_else(|| anyhow!("No config data received")) + } } pub fn convert_to_bulletin(new_bulletin: NewApiBulletin) -> Result { diff --git a/shared/config.toml b/shared/config.toml deleted file mode 100644 index ecfe3ae..0000000 --- a/shared/config.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Church configuration for bulletin generation -church_name = "Rockville-Tolland Seventh-Day Adventist Church" -# contact_phone = "555-123-4567" -# contact_website = "yourchurchwebsite.org" -# contact_youtube = "youtube.com/yourchurchchannel" -# contact_address = "123 Church St, Your City, ST 12345" \ No newline at end of file