Fetch the iCalendar file from an URL and show the meetings that are planned in the future. Because almost everything was touched, this commit also incorporates a cargo update.
179 lines
5.7 KiB
Rust
179 lines
5.7 KiB
Rust
extern crate console_error_panic_hook;
|
|
use anyhow::Context as _;
|
|
use chrono::{DateTime, Duration, Local};
|
|
use futures_util::FutureExt;
|
|
use icalendar::{Calendar, CalendarComponent::Event, Component as _, DatePerhapsTime, EventLike};
|
|
use std::{borrow::Cow, fmt::Debug, panic};
|
|
use tracing::{error, info};
|
|
use tracing_subscriber::{
|
|
fmt::{
|
|
format::{FmtSpan, Pretty},
|
|
time::UtcTime,
|
|
},
|
|
prelude::*,
|
|
};
|
|
use yew::{classes, function_component, html, Component, Context, Html, Properties};
|
|
|
|
const ICAL_URL: &str =
|
|
"https://data.local.cccsbg.at/remote.php/dav/public-calendars/jrFa8wz3zZQESKrQ?export";
|
|
|
|
#[derive(Clone, Debug, PartialEq, Properties)]
|
|
struct Meeting {
|
|
start: DateTime<Local>,
|
|
location: Cow<'static, str>,
|
|
summary: Cow<'static, str>,
|
|
description: Cow<'static, str>,
|
|
}
|
|
|
|
async fn fetch_ical(url: &'static str) -> anyhow::Result<Vec<Meeting>> {
|
|
let calendar = reqwest::get(url)
|
|
.await
|
|
.context("reqwest")?
|
|
.text()
|
|
.await
|
|
.context("text")?
|
|
.parse::<Calendar>()
|
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
|
|
|
let mut meetings: Vec<_> = calendar
|
|
.components
|
|
.into_iter()
|
|
.filter_map(|component| {
|
|
if let Event(event) = component {
|
|
let local = Local::now() - Duration::days(1);
|
|
if let DatePerhapsTime::DateTime(start) = event.get_start()? {
|
|
let start = start.try_into_utc()?.with_timezone(&Local);
|
|
|
|
if start < local {
|
|
return None;
|
|
}
|
|
|
|
return Some(Meeting {
|
|
start,
|
|
location: event.get_location()?.to_string().into(),
|
|
summary: event.get_summary()?.to_string().into(),
|
|
description: event.get_description()?.to_string().into(),
|
|
});
|
|
}
|
|
}
|
|
None
|
|
})
|
|
.collect();
|
|
|
|
meetings.sort_by(|a, b| a.start.cmp(&b.start));
|
|
|
|
Ok(meetings)
|
|
}
|
|
|
|
#[function_component]
|
|
fn MeetingBox(props: &Meeting) -> Html {
|
|
let upcoming = props.start > Local::now();
|
|
|
|
html! {
|
|
<div class={classes!("card", if upcoming { vec!["has-background-info", "has-text-info-invert"] } else { vec!["has-background-dark", "has-text-dark-invert"] })}>
|
|
<div class={classes!("card-header", if upcoming { "has-background-info-30" } else { "has-background-info-10" })}>
|
|
<p class={classes!("card-header-title", if upcoming { "has-text-grey-lighter" } else { "has-text-grey-dark" })}>{ props.start.to_string() }</p>
|
|
</div>
|
|
<div class="card-content">
|
|
<p>{ &props.summary }</p>
|
|
<p>{ &props.description }</p>
|
|
</div>
|
|
<div class="card-footer">
|
|
<p class="ml-5 my-1">{ &props.location }</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
enum Msg {
|
|
Fetched(Vec<Meeting>),
|
|
FetchError(String),
|
|
}
|
|
|
|
struct Meetings {
|
|
meetings: Option<Vec<Meeting>>,
|
|
}
|
|
|
|
impl Component for Meetings {
|
|
type Message = Msg;
|
|
type Properties = ();
|
|
|
|
fn create(ctx: &Context<Self>) -> Self {
|
|
let meetings = fetch_ical(ICAL_URL);
|
|
ctx.link().send_future(meetings.map(|res| match res {
|
|
Ok(meetings) => Msg::Fetched(meetings),
|
|
Err(error) => Msg::FetchError(error.to_string()),
|
|
}));
|
|
|
|
Self { meetings: None }
|
|
}
|
|
|
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
match msg {
|
|
Msg::Fetched(meetings) => self.meetings = Some(meetings),
|
|
Msg::FetchError(error) => error!(error),
|
|
}
|
|
true
|
|
}
|
|
|
|
fn view(&self, _ctx: &Context<Self>) -> Html {
|
|
html! {
|
|
if let Some(meetings) = &self.meetings {
|
|
if meetings.is_empty() {
|
|
<p>{ "empty ical" }</p>
|
|
} else {
|
|
<div class="grid">
|
|
{
|
|
meetings.iter().map(|row| {
|
|
html!{<MeetingBox start={row.start} location={row.location.clone()} summary={row.summary.clone()} description={row.description.clone()} />}
|
|
}).collect::<Html>()
|
|
}
|
|
</div>
|
|
}
|
|
} else {
|
|
<p>{ "fetching" }</p>
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[function_component]
|
|
fn App() -> Html {
|
|
html! {
|
|
<>
|
|
<div id="wrapper">
|
|
<div class="hero">
|
|
<div class="hero-body">
|
|
<h1 class="title">{ "¯\\_(ツ)_/¯ SRUG: Salzburg Rust User Group" }</h1>
|
|
</div>
|
|
</div>
|
|
<div class="section">
|
|
<p class="mb-6 is-family-monospace">{ "lazy_static!(std::collection::VecDeque<Wesen>), die gerne |etwas| unsafe { mit Rust machen } würde.await?;" }</p>
|
|
<Meetings />
|
|
</div>
|
|
</div>
|
|
<footer class="footer has-text-centered">
|
|
<p>{ "CCCSBG Space in der Arge Kultur, Ulrike-Gschwandtner-Straße 5, 5020 Salzburg" }</p>
|
|
<a href="https://cccsbg.at">{ "CCCSBG" }</a>
|
|
</footer>
|
|
</>
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let fmt_layer = tracing_subscriber::fmt::layer()
|
|
.with_ansi(false)
|
|
.with_timer(UtcTime::rfc_3339())
|
|
.with_writer(tracing_web::MakeConsoleWriter)
|
|
.with_span_events(FmtSpan::ACTIVE);
|
|
let perf_layer = tracing_web::performance_layer().with_details_from_fields(Pretty::default());
|
|
panic::set_hook(Box::new(console_error_panic_hook::hook));
|
|
|
|
tracing_subscriber::registry()
|
|
.with(fmt_layer)
|
|
.with(perf_layer)
|
|
.init();
|
|
info!("starting client",);
|
|
|
|
yew::Renderer::<App>::new().render();
|
|
}
|