Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
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]
|
[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"] }
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[tools]
|
|
||||||
sass = "1.43.5"
|
|
168
src/main.rs
168
src/main.rs
|
@ -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>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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! {
|
html! {
|
||||||
|
if let Some(meetings) = &self.meetings {
|
||||||
|
if meetings.is_empty() {
|
||||||
|
<p>{ "empty ical" }</p>
|
||||||
|
} else {
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{
|
{
|
||||||
props.meetings.iter().map(|row| {
|
meetings.iter().map(|row| {
|
||||||
html!{<MeetingBox date={row.date.clone()} location={row.location.clone()} description={row.description.clone()} />}
|
html!{<MeetingBox start={row.start} location={row.location.clone()} summary={row.summary.clone()} description={row.description.clone()} />}
|
||||||
}).collect::<Html>()
|
}).collect::<Html>()
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
<p>{ "fetching" }</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue