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.
This commit is contained in:
parent
672afdda24
commit
d60d55ee66
4 changed files with 1836 additions and 230 deletions
1870
Cargo.lock
generated
1870
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
@ -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"] }
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[tools]
|
||||
sass = "1.43.5"
|
168
src/main.rs
168
src/main.rs
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue