1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use apalis_board_types::LogEntry;
4use chrono::{DateTime, Local, Utc};
5use leptos::{prelude::*, reactive::spawn_local};
6use serde::{Deserialize, Serialize};
7
8pub mod api;
9pub mod components;
10pub mod pages;
11pub mod translate;
12
13#[allow(deprecated)]
14pub mod locales {
15 leptos_i18n::load_locales!();
17}
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
20pub struct User {
21 pub id: String,
22 pub username: String,
23}
24
25type UserSignal = RwSignal<Option<User>>;
26pub fn use_user_signal() -> UserSignal {
27 use_context::<UserSignal>().expect("UserSignal")
28}
29
30pub fn use_sse_signal() -> RwSignal<Vec<String>> {
31 use_context::<RwSignal<Vec<String>>>().expect("SSE Signal")
32}
33
34#[derive(Clone, Copy)]
35pub struct SseProvider {
36 event_source: RwSignal<LogEntry>,
37 is_healthy: RwSignal<bool>,
38}
39
40impl SseProvider {
41 pub fn event_source(&self) -> RwSignal<LogEntry> {
42 self.event_source
43 }
44
45 pub fn is_healthy(&self) -> RwSignal<bool> {
46 self.is_healthy
47 }
48}
49
50pub fn use_sse_provider() -> SseProvider {
51 use_context::<SseProvider>().expect("SSE Provider")
52}
53
54pub fn create_sse_resource(url: &str) -> SseProvider {
55 use futures::StreamExt;
56 let data = RwSignal::new(LogEntry::default());
57 let mut source = gloo_net::eventsource::futures::EventSource::new(url)
58 .expect("couldn't connect to SSE stream");
59 let mut stream = source
60 .subscribe("message")
61 .unwrap()
62 .map(|value| match value {
63 Ok(value) => Ok(serde_json::from_str::<LogEntry>(
64 &value.1.data().as_string().expect("expected string value"),
65 )
66 .expect("couldn't parse message")),
67 Err(e) => Err(e),
68 });
69 let is_healthy = RwSignal::new(true);
70 spawn_local(async move {
71 while let Some(next_value) = stream.next().await {
72 if let Ok(log_entry) = next_value {
73 data.set(log_entry);
74 } else {
75 is_healthy.set(false);
76 source.close();
77 break;
78 }
79 }
80 });
81 SseProvider {
82 event_source: data,
83 is_healthy,
84 }
85}
86
87pub fn relative_timestamp(timestamp: u64) -> String {
88 let now = Utc::now().timestamp() as u64;
89
90 match timestamp.cmp(&now) {
91 std::cmp::Ordering::Greater => {
92 let future_diff = timestamp - now;
93 match future_diff {
94 0..=59 => "in a few seconds".to_string(),
95 60..=3599 => format!("in {} minutes", future_diff / 60),
96 3600..=86399 => format!("in {} hours", future_diff / 3600),
97 86400..=2_592_000 => format!("in {} days", future_diff / 86400),
98 _ => {
99 let datetime = DateTime::<Utc>::from_timestamp(timestamp as i64, 0)
100 .expect("Invalid timestamp");
101 let local: DateTime<Local> = datetime.into();
102 format!("on {}", local.format("%B %d, %Y"))
103 }
104 }
105 }
106 _ => {
107 let diff = now.saturating_sub(timestamp);
108 match diff {
109 0..=59 => "just now".to_string(),
110 60..=3599 => format!("{} minutes ago", diff / 60),
111 3600..=86399 => format!("{} hours ago", diff / 3600),
112 86400..=2_592_000 => format!("{} days ago", diff / 86400),
113 _ => {
114 let datetime = DateTime::<Utc>::from_timestamp(timestamp as i64, 0)
115 .expect("Invalid timestamp");
116 let local: DateTime<Local> = datetime.into();
117 local.format("%B %d, %Y").to_string()
118 }
119 }
120 }
121 }
122}
123
124pub type RawTask = apalis_core::task::Task<serde_json::Value, serde_json::Value, String>;