Compare commits

...

1 commit
v0.1.1 ... main

Author SHA1 Message Date
d60d55ee66 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
4 changed files with 1836 additions and 230 deletions

1870
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,18 @@
[package] [package]
name = "srug_website" name = "srug_website"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
time = { version = "0.3.36", features = ["macros", "formatting", "local-offset", "wasm-bindgen"] } anyhow = "1.0.98"
chrono = { version = "0.4.40", features = ["wasmbind"] }
console_error_panic_hook = "0.1.7"
futures-util = "0.3.31"
icalendar = { version = "0.16.1", features = ["chrono-tz"] }
reqwest = "0.12.4"
time = { version = "0.3.41", features = ["wasm-bindgen"] }
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"] } yew = { version = "0.21.0", features = ["csr"] }

View file

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

View file

@ -1,27 +1,81 @@
use time::{ extern crate console_error_panic_hook;
macros::{datetime, format_description}, use anyhow::Context as _;
OffsetDateTime, 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, 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 { struct Meeting {
date: OffsetDateTime, start: DateTime<Local>,
location: String, location: Cow<'static, str>,
description: String, 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] #[function_component]
fn MeetingBox(props: &Meeting) -> Html { fn MeetingBox(props: &Meeting) -> Html {
let format = format_description!("[year]-[month]-[day] [hour]:[minute]"); let upcoming = props.start > Local::now();
let upcoming = props.date > OffsetDateTime::now_local().unwrap();
html! { 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", 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" })}> <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>
<div class="card-content"> <div class="card-content">
<p>{ &props.summary }</p>
<p>{ &props.description }</p> <p>{ &props.description }</p>
</div> </div>
<div class="card-footer"> <div class="card-footer">
@ -31,54 +85,60 @@ fn MeetingBox(props: &Meeting) -> Html {
} }
} }
#[derive(Properties, PartialEq)] enum Msg {
struct Meetings { Fetched(Vec<Meeting>),
meetings: Vec<Meeting>, FetchError(String),
} }
#[function_component] struct Meetings {
fn MeetingsTable(props: &Meetings) -> Html { meetings: Option<Vec<Meeting>>,
html! { }
<div class="grid">
{ impl Component for Meetings {
props.meetings.iter().map(|row| { type Message = Msg;
html!{<MeetingBox date={row.date.clone()} location={row.location.clone()} description={row.description.clone()} />} type Properties = ();
}).collect::<Html>()
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>
} }
</div> }
} }
} }
#[function_component] #[function_component]
fn App() -> Html { 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! { html! {
<> <>
<div id="wrapper"> <div id="wrapper">
@ -89,7 +149,7 @@ fn App() -> Html {
</div> </div>
<div class="section"> <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> <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>
</div> </div>
<footer class="footer has-text-centered"> <footer class="footer has-text-centered">
@ -101,5 +161,19 @@ fn App() -> Html {
} }
fn main() { 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(); yew::Renderer::<App>::new().render();
} }