Add LICENSE and update README for new repository
- Add MIT LICENSE file - Update README.md to reflect new API integration - Include all recent changes to migrate from PocketBase to church API 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fdfd83c747
commit
d911689e3a
2162
Cargo.lock
generated
2162
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -21,8 +21,8 @@ announcements, and information in a beautiful and engaging way.
|
|||
|
||||
[dependencies]
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
iced = { git = "https://github.com/iced-rs/iced.git", features = ["image", "tokio", "advanced", "debug", "system"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
iced = { version = "0.13", features = ["image", "tokio", "advanced", "debug", "system"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
|
@ -36,5 +36,6 @@ toml = "0.8"
|
|||
dirs = "5.0"
|
||||
ril = { version = "0.10", features = ["all"] }
|
||||
infer = "0.15"
|
||||
church-core = { path = "../church-core" }
|
||||
[package.metadata.iced.assets]
|
||||
icon = "icons/appicon.png"
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Rockville Tolland Seventh-day Adventist Church
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -14,18 +14,18 @@ A modern digital signage application for displaying church events, built with Ru
|
|||
## Requirements
|
||||
|
||||
- Rust 1.70 or higher
|
||||
- A running Pocketbase instance with events collection
|
||||
- Access to the church API for events data
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `config.toml` file in the application directory with the following settings:
|
||||
|
||||
```toml
|
||||
pocketbase_url = "http://your-pocketbase-url"
|
||||
api_url = "https://api.rockvilletollandsda.church/api"
|
||||
window_width = 1920
|
||||
window_height = 1080
|
||||
slide_interval_secs = 10
|
||||
refresh_interval_mins = 5
|
||||
slide_interval_seconds = 10
|
||||
refresh_interval_minutes = 5
|
||||
```
|
||||
|
||||
## Building
|
||||
|
|
|
@ -40,7 +40,7 @@ impl Settings {
|
|||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
api_url: String::from("https://api.rockvilletollandsda.church"),
|
||||
api_url: String::from("https://api.rockvilletollandsda.church/api"),
|
||||
window_width: 1920,
|
||||
window_height: 1080,
|
||||
slide_interval_seconds: 10,
|
||||
|
|
237
src/main.rs
237
src/main.rs
|
@ -1,18 +1,16 @@
|
|||
mod config;
|
||||
mod pocketbase;
|
||||
|
||||
use crate::pocketbase::ApiEvent;
|
||||
use church_core::models::event::Event;
|
||||
use church_core::client::ChurchApiClient;
|
||||
use church_core::config::ChurchCoreConfig;
|
||||
use iced::widget::{column, row, image, container, text};
|
||||
use iced::{
|
||||
window, Element,
|
||||
Length, Settings, Subscription, Theme, Task,
|
||||
};
|
||||
use iced::executor;
|
||||
pub use iced::Program as IcedProgram;
|
||||
use iced::Color;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::time::Instant;
|
||||
use iced::window::settings::PlatformSpecific;
|
||||
|
||||
|
||||
|
||||
|
@ -24,8 +22,10 @@ static SETTINGS: Lazy<config::Settings> = Lazy::new(|| {
|
|||
})
|
||||
});
|
||||
|
||||
static API_CLIENT: Lazy<pocketbase::ApiClient> = Lazy::new(|| {
|
||||
pocketbase::ApiClient::new(SETTINGS.api_url.clone())
|
||||
static API_CLIENT: Lazy<ChurchApiClient> = Lazy::new(|| {
|
||||
let config = ChurchCoreConfig::new()
|
||||
.with_base_url(SETTINGS.api_url.clone());
|
||||
ChurchApiClient::new(config).expect("Failed to create API client")
|
||||
});
|
||||
|
||||
// Define some constants for styling
|
||||
|
@ -54,20 +54,6 @@ struct DigitalSign {
|
|||
is_fetching: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Event {
|
||||
title: String,
|
||||
description: String,
|
||||
start_time: String,
|
||||
end_time: String,
|
||||
date: String,
|
||||
location: String,
|
||||
//location_url: Option<String>,
|
||||
image_url: Option<String>,
|
||||
category: String,
|
||||
//is_featured: bool,
|
||||
timestamp: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
|
@ -77,43 +63,44 @@ enum Message {
|
|||
ImageLoaded(String, image::Handle),
|
||||
}
|
||||
|
||||
impl IcedProgram for DigitalSign {
|
||||
type Message = Message;
|
||||
type Theme = Theme;
|
||||
type Executor = executor::Default;
|
||||
type State = Self;
|
||||
type Renderer = iced::Renderer;
|
||||
impl DigitalSign {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
let mut app = Self::default();
|
||||
app.is_fetching = true;
|
||||
let task = Task::perform(fetch_events(), Message::handle_result);
|
||||
(app, task)
|
||||
}
|
||||
|
||||
fn title(&self, _state: &Self::State, _window_id: window::Id) -> String {
|
||||
fn title(&self) -> String {
|
||||
String::from("Beacon")
|
||||
}
|
||||
|
||||
fn update(&self, state: &mut Self::State, message: Message) -> Task<Message> {
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Tick => {
|
||||
let mut tasks = vec![];
|
||||
state.loading_frame = (state.loading_frame + 1) % LOADING_FRAMES.len();
|
||||
self.loading_frame = (self.loading_frame + 1) % LOADING_FRAMES.len();
|
||||
|
||||
if state.should_refresh() && !state.is_fetching {
|
||||
if self.should_refresh() && !self.is_fetching {
|
||||
tracing::info!("Refresh needed, starting event fetch");
|
||||
state.is_fetching = true;
|
||||
self.is_fetching = true;
|
||||
tasks.push(Task::perform(fetch_events(), Message::handle_result));
|
||||
}
|
||||
|
||||
if !state.events.is_empty()
|
||||
&& Instant::now().duration_since(state.last_update) >= SETTINGS.slide_interval()
|
||||
if !self.events.is_empty()
|
||||
&& Instant::now().duration_since(self.last_update) >= SETTINGS.slide_interval()
|
||||
{
|
||||
let next_index = (state.current_event_index + 1) % state.events.len();
|
||||
let next_index = (self.current_event_index + 1) % self.events.len();
|
||||
tracing::info!("Updating current event index from {} to {}",
|
||||
state.current_event_index,
|
||||
self.current_event_index,
|
||||
next_index
|
||||
);
|
||||
|
||||
// Clear all images that aren't needed anymore
|
||||
let mut urls_to_remove = Vec::new();
|
||||
for url in state.loaded_images.keys() {
|
||||
let is_needed = state.events.iter().any(|e| {
|
||||
e.image_url.as_ref().map_or(false, |event_url| event_url == url)
|
||||
for url in self.loaded_images.keys() {
|
||||
let is_needed = self.events.iter().any(|e| {
|
||||
e.image.as_ref().map_or(false, |event_url| event_url == url)
|
||||
});
|
||||
if !is_needed {
|
||||
urls_to_remove.push(url.clone());
|
||||
|
@ -121,17 +108,17 @@ impl IcedProgram for DigitalSign {
|
|||
}
|
||||
for url in urls_to_remove {
|
||||
tracing::info!("Removing unused image: {}", url);
|
||||
state.loaded_images.remove(&url);
|
||||
self.loaded_images.remove(&url);
|
||||
}
|
||||
|
||||
// Update current index and load new image if needed
|
||||
state.current_event_index = next_index;
|
||||
state.last_update = Instant::now();
|
||||
self.current_event_index = next_index;
|
||||
self.last_update = Instant::now();
|
||||
|
||||
if let Some(current_event) = state.events.get(state.current_event_index) {
|
||||
if let Some(url) = ¤t_event.image_url {
|
||||
if let Some(current_event) = self.events.get(self.current_event_index) {
|
||||
if let Some(url) = ¤t_event.image {
|
||||
let url_clone = url.clone();
|
||||
if !state.loaded_images.contains_key(&url_clone) {
|
||||
if !self.loaded_images.contains_key(&url_clone) {
|
||||
tracing::info!("Starting image load for new current event: {}", url_clone);
|
||||
let url_for_closure = url_clone.clone();
|
||||
tasks.push(Task::perform(
|
||||
|
@ -156,26 +143,26 @@ impl IcedProgram for DigitalSign {
|
|||
tracing::info!("Events loaded: {} events", events.len());
|
||||
|
||||
// Clear all existing images as we have a new set of events
|
||||
state.loaded_images.clear();
|
||||
self.loaded_images.clear();
|
||||
tracing::info!("Cleared all existing images");
|
||||
|
||||
state.events = events;
|
||||
self.events = events;
|
||||
|
||||
// Reset current event index if needed
|
||||
if state.current_event_index >= state.events.len() && !state.events.is_empty() {
|
||||
tracing::info!("Resetting current event index from {} to 0", state.current_event_index);
|
||||
state.current_event_index = 0;
|
||||
if self.current_event_index >= self.events.len() && !self.events.is_empty() {
|
||||
tracing::info!("Resetting current event index from {} to 0", self.current_event_index);
|
||||
self.current_event_index = 0;
|
||||
}
|
||||
|
||||
state.last_refresh = Instant::now();
|
||||
state.is_fetching = false;
|
||||
self.last_refresh = Instant::now();
|
||||
self.is_fetching = false;
|
||||
|
||||
// Load all images in parallel
|
||||
let mut image_tasks = Vec::new();
|
||||
|
||||
// First, add the current event's image if it exists
|
||||
if let Some(event) = state.events.get(state.current_event_index) {
|
||||
if let Some(url) = &event.image_url {
|
||||
if let Some(event) = self.events.get(self.current_event_index) {
|
||||
if let Some(url) = &event.image {
|
||||
tracing::info!("Starting immediate load for current image: {}", url);
|
||||
let url_clone = url.clone();
|
||||
image_tasks.push(Task::perform(
|
||||
|
@ -186,9 +173,9 @@ impl IcedProgram for DigitalSign {
|
|||
}
|
||||
|
||||
// Then queue the rest of the images
|
||||
for (index, event) in state.events.iter().enumerate() {
|
||||
if index != state.current_event_index {
|
||||
if let Some(url) = &event.image_url {
|
||||
for (index, event) in self.events.iter().enumerate() {
|
||||
if index != self.current_event_index {
|
||||
if let Some(url) = &event.image {
|
||||
tracing::info!("Queueing image preload for: {}", url);
|
||||
let url_clone = url.clone();
|
||||
image_tasks.push(Task::perform(
|
||||
|
@ -208,23 +195,25 @@ impl IcedProgram for DigitalSign {
|
|||
}
|
||||
Message::ImageLoaded(url, handle) => {
|
||||
tracing::info!("Image loaded: {}", url);
|
||||
state.loaded_images.insert(url, handle);
|
||||
self.loaded_images.insert(url, handle);
|
||||
Task::none()
|
||||
}
|
||||
Message::Error(error) => {
|
||||
tracing::error!("Error: {}", error);
|
||||
state.is_fetching = false;
|
||||
self.is_fetching = false;
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view<'a>(
|
||||
&self,
|
||||
state: &'a Self::State,
|
||||
_window_id: window::Id,
|
||||
) -> Element<'a, Message, Theme, Self::Renderer> {
|
||||
let content: Element<'a, Message, Theme, Self::Renderer> = if let Some(event) = state.events.get(state.current_event_index) {
|
||||
fn view(&self) -> Element<Message> {
|
||||
let content: Element<Message> = if let Some(event) = self.events.get(self.current_event_index) {
|
||||
let formatted_date = event.formatted_date();
|
||||
let formatted_start_time = event.formatted_start_time();
|
||||
let formatted_end_time = event.formatted_end_time();
|
||||
let clean_description = event.clean_description();
|
||||
let category_string = event.category.to_string();
|
||||
|
||||
let mut main_column = column![].spacing(40).padding(60).width(Length::Fill);
|
||||
|
||||
// Left column with title and image
|
||||
|
@ -240,8 +229,8 @@ impl IcedProgram for DigitalSign {
|
|||
|
||||
// Image container with enhanced styling
|
||||
container(
|
||||
if let Some(ref image_url) = event.image_url {
|
||||
if let Some(handle) = state.loaded_images.get(image_url) {
|
||||
if let Some(ref image_url) = event.image {
|
||||
if let Some(handle) = self.loaded_images.get(image_url) {
|
||||
container(
|
||||
image::Image::new(handle.clone())
|
||||
.width(Length::Fixed(900.0))
|
||||
|
@ -254,7 +243,7 @@ impl IcedProgram for DigitalSign {
|
|||
} else {
|
||||
container(
|
||||
column![
|
||||
text(LOADING_FRAMES[state.loading_frame])
|
||||
text(LOADING_FRAMES[self.loading_frame])
|
||||
.size(80)
|
||||
.style(|_: &Theme| text::Style { color: Some(ACCENT_COLOR), ..Default::default() }),
|
||||
text("Loading image...")
|
||||
|
@ -286,7 +275,7 @@ impl IcedProgram for DigitalSign {
|
|||
let right_column = column![
|
||||
// Category badge with gradient-like effect
|
||||
container(
|
||||
text(event.category.to_uppercase())
|
||||
text(category_string.to_uppercase())
|
||||
.size(36)
|
||||
.style(|_: &Theme| text::Style { color: Some(TEXT_COLOR), ..Default::default() })
|
||||
)
|
||||
|
@ -299,10 +288,10 @@ impl IcedProgram for DigitalSign {
|
|||
// Date and time with enhanced colors
|
||||
container(
|
||||
column![
|
||||
text(&event.date)
|
||||
text(formatted_date)
|
||||
.size(64)
|
||||
.style(|_: &Theme| text::Style { color: Some(DATE_COLOR), ..Default::default() }),
|
||||
text(format!("{} - {}", event.start_time, event.end_time))
|
||||
text(format!("{} - {}", formatted_start_time, formatted_end_time))
|
||||
.size(56)
|
||||
.style(|_: &Theme| text::Style { color: Some(TIME_COLOR), ..Default::default() })
|
||||
]
|
||||
|
@ -332,7 +321,7 @@ impl IcedProgram for DigitalSign {
|
|||
|
||||
// Description with styled background
|
||||
container(
|
||||
text(&event.description)
|
||||
text(clean_description)
|
||||
.size(44)
|
||||
.style(|_: &Theme| text::Style { color: Some(TEXT_COLOR), ..Default::default() })
|
||||
)
|
||||
|
@ -386,12 +375,12 @@ impl IcedProgram for DigitalSign {
|
|||
.into()
|
||||
}
|
||||
|
||||
fn subscription(&self, _state: &Self::State) -> Subscription<Message> {
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
iced::time::every(std::time::Duration::from_millis(100))
|
||||
.map(|_| Message::Tick)
|
||||
}
|
||||
|
||||
fn theme(&self, _state: &Self::State, _window_id: window::Id) -> Theme {
|
||||
fn theme(&self) -> Theme {
|
||||
Theme::Dark
|
||||
}
|
||||
}
|
||||
|
@ -407,36 +396,30 @@ impl Message {
|
|||
|
||||
async fn fetch_events() -> Result<Vec<Event>, anyhow::Error> {
|
||||
tracing::info!("Starting to fetch upcoming events from API");
|
||||
let api_events = match API_CLIENT.fetch_events().await {
|
||||
let mut events = match API_CLIENT.get_upcoming_events(None).await {
|
||||
Ok(events) => {
|
||||
tracing::info!("Successfully fetched {} upcoming events from API", events.len());
|
||||
tracing::info!("Successfully fetched {} events from API", events.len());
|
||||
events
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to fetch events from API: {}", e);
|
||||
return Err(e);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Convert API events to display events (no filtering needed since /upcoming endpoint handles it)
|
||||
let mut events: Vec<Event> = api_events
|
||||
.into_iter()
|
||||
.map(Event::from)
|
||||
.collect();
|
||||
|
||||
if events.is_empty() {
|
||||
tracing::warn!("No upcoming events found");
|
||||
} else {
|
||||
tracing::info!(
|
||||
"Found {} upcoming events, from {} to {}",
|
||||
events.len(),
|
||||
events.first().map(|e| e.date.as_str()).unwrap_or("unknown"),
|
||||
events.last().map(|e| e.date.as_str()).unwrap_or("unknown")
|
||||
events.first().map(|e| e.formatted_date()).unwrap_or("unknown".to_string()),
|
||||
events.last().map(|e| e.formatted_date()).unwrap_or("unknown".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by start time (API should already provide them sorted, but ensure consistency)
|
||||
events.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
||||
// Sort by start time
|
||||
events.sort_by(|a, b| a.start_time.cmp(&b.start_time));
|
||||
tracing::info!("Processed {} upcoming events", events.len());
|
||||
Ok(events)
|
||||
}
|
||||
|
@ -488,38 +471,6 @@ async fn load_image(url: String) -> image::Handle {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ApiEvent> for Event {
|
||||
fn from(event: ApiEvent) -> Self {
|
||||
let clean_description = html2text::from_read(event.description.as_bytes(), 80)
|
||||
.replace('\n', " ")
|
||||
.split_whitespace()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
|
||||
let date = event.start_time.format("%A, %B %d, %Y").to_string();
|
||||
let start_time = event.start_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string();
|
||||
let end_time = event.end_time.format("%I:%M %p").to_string().trim_start_matches('0').to_string();
|
||||
|
||||
let image_url = event.image.clone();
|
||||
if let Some(ref url) = image_url {
|
||||
tracing::info!("Using image URL: {}", url);
|
||||
}
|
||||
|
||||
Self {
|
||||
title: event.title,
|
||||
description: clean_description,
|
||||
start_time,
|
||||
end_time,
|
||||
date,
|
||||
location: event.location,
|
||||
//location_url: event.location_url,
|
||||
image_url,
|
||||
category: event.category,
|
||||
//is_featured: event.is_featured,
|
||||
timestamp: event.start_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> iced::Result {
|
||||
tracing_subscriber::fmt()
|
||||
|
@ -574,23 +525,6 @@ fn main() -> iced::Result {
|
|||
}
|
||||
};
|
||||
|
||||
let window_settings = window::Settings {
|
||||
size: iced::Size::new(SETTINGS.window_width as f32, SETTINGS.window_height as f32),
|
||||
position: window::Position::Centered,
|
||||
fullscreen: true,
|
||||
resizable: false,
|
||||
decorations: false,
|
||||
icon: icon_data,
|
||||
#[cfg(target_os = "macos")]
|
||||
platform_specific: PlatformSpecific {
|
||||
title_hidden: true,
|
||||
titlebar_transparent: true,
|
||||
fullsize_content_view: true,
|
||||
},
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
platform_specific: PlatformSpecific::default(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Load additional fonts for better Unicode support
|
||||
let fonts = vec![
|
||||
|
@ -605,34 +539,17 @@ fn main() -> iced::Result {
|
|||
].into_iter().filter_map(|f| f).map(|bytes| std::borrow::Cow::Owned(bytes)).collect();
|
||||
|
||||
let settings = Settings {
|
||||
// window: window_settings,
|
||||
//flags: (),
|
||||
fonts,
|
||||
default_font: iced::Font::default(),
|
||||
antialiasing: true,
|
||||
default_text_size: iced::Pixels(16.0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Create the initial state and start loading events
|
||||
let mut app = DigitalSign::default();
|
||||
app.is_fetching = true;
|
||||
|
||||
DigitalSign::run_with(
|
||||
app,
|
||||
settings,
|
||||
Some(window_settings),
|
||||
|| {
|
||||
let mut state = DigitalSign::default();
|
||||
state.is_fetching = true;
|
||||
(
|
||||
state,
|
||||
Task::perform(
|
||||
fetch_events(),
|
||||
Message::handle_result
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
iced::application("Beacon", DigitalSign::update, DigitalSign::view)
|
||||
.subscription(DigitalSign::subscription)
|
||||
.theme(DigitalSign::theme)
|
||||
.settings(settings)
|
||||
.window_size(iced::Size::new(800.0, 600.0))
|
||||
.run_with(DigitalSign::new)
|
||||
}
|
||||
|
||||
impl DigitalSign {
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
const API_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiEvent {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub start_time: DateTime<Utc>,
|
||||
pub end_time: DateTime<Utc>,
|
||||
pub location: String,
|
||||
pub location_url: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
pub category: String,
|
||||
pub is_featured: bool,
|
||||
pub recurring_type: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApiClient {
|
||||
client: reqwest::Client,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub fn new(base_url: String) -> Self {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(API_TIMEOUT)
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self { client, base_url }
|
||||
}
|
||||
|
||||
pub async fn fetch_events(&self) -> Result<Vec<ApiEvent>> {
|
||||
let url = format!("{}/api/events/upcoming", self.base_url);
|
||||
tracing::info!("Fetching events from URL: {}", url);
|
||||
|
||||
let response = match self.client.get(&url)
|
||||
.header("Cache-Control", "max-age=60") // Cache for 60 seconds
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(resp) => {
|
||||
tracing::info!("Got response with status: {}", resp.status());
|
||||
resp
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("HTTP request failed: {}", e);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
let response = match response.error_for_status() {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
tracing::error!("HTTP error status: {}", e);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ApiResponse {
|
||||
success: bool,
|
||||
data: Vec<ApiEvent>,
|
||||
}
|
||||
|
||||
match response.json().await {
|
||||
Ok(api_response) => {
|
||||
let ApiResponse { success, data } = api_response;
|
||||
if success {
|
||||
tracing::info!("Successfully parsed {} events from response", data.len());
|
||||
Ok(data)
|
||||
} else {
|
||||
tracing::error!("API returned success: false");
|
||||
Err(anyhow::anyhow!("API request failed"))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to parse JSON response: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue