Compare commits

...

2 commits
v0.1.0 ... 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
672afdda24 Add new meeting 2025-04-11 18:54:54 +02:00
4 changed files with 1836 additions and 225 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,49 +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 {
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>()
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>
}
</div>
}
}
}
#[function_component]
fn App() -> Html {
let meetings = vec![
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">
@ -84,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">
@ -96,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();
}