srug-website/src/main.rs
finga d60d55ee66
All checks were successful
ci/woodpecker/push/release Pipeline was successful
ci/woodpecker/push/checks Pipeline was successful
Use the "official" calendar 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.
2025-04-20 21:35:01 +02:00

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();
}