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