Compare commits

..

1 commit
v0.1.1 ... main

Author SHA1 Message Date
d60d55ee66 Use the "official" calendar for meetings
All checks were successful
ci/woodpecker/push/release Pipeline was successful
ci/woodpecker/push/checks Pipeline was successful
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]
name = "srug_website"
version = "0.1.0"
edition = "2021"
version = "0.2.0"
edition = "2024"
[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"] }

View file

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

View file

@ -1,27 +1,81 @@
use time::{
macros::{datetime, format_description},
OffsetDateTime,
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, 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>,
}
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 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 +85,60 @@ fn MeetingBox(props: &Meeting) -> Html {
}
}
#[derive(Properties, PartialEq)]
struct Meetings {
meetings: Vec<Meeting>,
enum Msg {
Fetched(Vec<Meeting>),
FetchError(String),
}
#[function_component]
fn MeetingsTable(props: &Meetings) -> Html {
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">
{
props.meetings.iter().map(|row| {
html!{<MeetingBox date={row.date.clone()} location={row.location.clone()} description={row.description.clone()} />}
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 {
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 +149,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 +161,19 @@ 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());
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();
}