Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
062da69a94 |
4 changed files with 1851 additions and 232 deletions
1885
Cargo.lock
generated
1885
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
18
Cargo.toml
18
Cargo.toml
|
@ -1,8 +1,20 @@
|
|||
[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"
|
||||
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"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
|
||||
|
|
|
@ -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