Use the "official" calendar URL for meetings

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.
This commit is contained in:
finga 2025-04-18 19:38:48 +02:00
parent 672afdda24
commit 081082e542
4 changed files with 1843 additions and 228 deletions

1880
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -4,5 +4,16 @@ version = "0.1.0"
edition = "2021"
[dependencies]
time = { version = "0.3.36", features = ["macros", "formatting", "local-offset", "wasm-bindgen"] }
anyhow = "1.0.98"
chrono = "0.4.40"
futures-util = "0.3.31"
icalendar = { version = "0.16.1", features = ["chrono-tz"] }
reqwest = "0.12.4"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["time"] }
tracing-web = "0.1.3"
wasm-bindgen = "0.2.100"
yew = { version = "0.21.0", features = ["csr"] }
[dev-dependencies]
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }

View file

@ -1,2 +0,0 @@
[tools]
sass = "1.43.5"

View file

@ -1,27 +1,40 @@
use time::{
macros::{datetime, format_description},
OffsetDateTime,
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};
use tracing::{error, info};
use tracing_subscriber::{
fmt::{
format::{FmtSpan, Pretty},
time::UtcTime,
},
prelude::*,
};
use yew::{classes, function_component, html, Html, Properties};
use yew::{classes, function_component, html, Component, Context, Html, Properties};
#[derive(Properties, PartialEq)]
const ICAL_URL: &str =
"https://data.local.cccsbg.at/remote.php/dav/public-calendars/jrFa8wz3zZQESKrQ?export";
#[derive(Clone, Debug, PartialEq, Properties)]
struct Meeting {
date: OffsetDateTime,
location: String,
description: String,
start: DateTime<Local>,
location: Cow<'static, str>,
summary: Cow<'static, str>,
description: Cow<'static, str>,
}
#[function_component]
fn MeetingBox(props: &Meeting) -> Html {
let format = format_description!("[year]-[month]-[day] [hour]:[minute]");
let upcoming = props.date > OffsetDateTime::now_local().unwrap();
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.date.format(&format).unwrap_or("something went utterly wrong".into()) }</p>
<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">
@ -31,54 +44,100 @@ fn MeetingBox(props: &Meeting) -> Html {
}
}
#[derive(Properties, PartialEq)]
struct Meetings {
meetings: Vec<Meeting>,
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 MeetingsTable(props: &Meetings) -> Html {
html! {
<div class="grid">
{
props.meetings.iter().map(|row| {
html!{<MeetingBox date={row.date.clone()} location={row.location.clone()} description={row.description.clone()} />}
}).collect::<Html>()
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.clone()} location={row.location.clone()} summary={row.summary.clone()} description={row.description.clone()} />}
}).collect::<Html>()
}
</div>
}
} else {
<p>{ "fetching"}</p>
}
</div>
}
}
}
#[function_component]
fn App() -> Html {
let meetings = vec![
Meeting {
date: datetime!(2025-04-15 18:00 +2:00),
location: "CCCSBG Space".into(),
description: "Reboot the website or telnet?".into(),
},
Meeting {
date: datetime!(2024-05-23 18:00 +2:00),
location: "CCCSBG Space".into(),
description: "Something.await?".into(),
},
Meeting {
date: datetime!(2024-04-30 18:00 +2:00),
location: "CCCSBG Space".into(),
description: "All your web are belong to us!".into(),
},
Meeting {
date: datetime!(2024-01-23 18:00 +1:00),
location: "CCCSBG Space".into(),
description: "Hello World, again!".into(),
},
Meeting {
date: datetime!(2023-11-28 18:30 +1:00),
location: "CCCSBG Space".into(),
description: "Hello World!".into(),
},
];
html! {
<>
<div id="wrapper">
@ -89,7 +148,7 @@ fn App() -> Html {
</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>
<MeetingsTable meetings={meetings} />
<Meetings />
</div>
</div>
<footer class="footer has-text-centered">
@ -101,5 +160,18 @@ fn App() -> Html {
}
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());
tracing_subscriber::registry()
.with(fmt_layer)
.with(perf_layer)
.init();
info!("starting client",);
yew::Renderer::<App>::new().render();
}