use std::{ pin::Pin, sync::Arc, time::{Duration, Instant}, }; use assetserver::Asset; use chrono::{DateTime, TimeDelta, Utc}; use maud::{Markup, PreEscaped, html}; use page::{DeviceType, Page, RequestContext}; use parking_lot::Mutex; use serde::Deserialize; use tracing::{debug, warn}; use crate::{ components::{ md::{Markdown, meta_from_markdown}, misc::{Backlinks, FarLink}, }, routes::assets::Image_Icon, }; #[derive(Debug, Deserialize)] struct HandoutEntry { title: String, group: String, handout: String, solutions: Option, } struct CachedRequestInner { last_fetch: DateTime, last_value: Option>, } pub struct CachedRequest { inner: Mutex>, ttl: TimeDelta, get: Box Pin + Send + Sync>> + Send + Sync>, } impl CachedRequest { pub fn new( ttl: TimeDelta, get: Box Pin + Send + Sync>> + Send + Sync>, ) -> Arc { Arc::new(Self { get, ttl, inner: Mutex::new(CachedRequestInner { last_fetch: Utc::now(), last_value: None, }), }) } pub async fn get(self: Arc) -> Arc { let now = Utc::now(); let expires = self.inner.lock().last_fetch + self.ttl; if now < expires && let Some(last_value) = self.inner.lock().last_value.clone() { return last_value; } let res = Arc::new((self.get)().await); let mut inner = self.inner.lock(); inner.last_fetch = now; inner.last_value = Some(res.clone()); return res; } pub async fn autoget(self: Arc, interval: Duration) { loop { { let now = Utc::now(); let res = Arc::new((self.get)().await); let mut inner = self.inner.lock(); inner.last_fetch = now; inner.last_value = Some(res.clone()); } tokio::time::sleep(interval).await; } } } async fn get_index() -> Result, reqwest::Error> { let start = Instant::now(); let res = reqwest::get( "https://git.betalupi.com/api/packages/Mark/generic/ormc-handouts/latest/index.json", ) .await; let res = match res { Ok(x) => x, Err(err) => { warn!("Error while getting index: {err:?}"); return Err(err); } }; let mut res: Vec = res.json().await?; res.sort_by_key(|x| x.title.clone()); debug!( message = "Fetched handout index", n_handouts = res.len(), time_ms = start.elapsed().as_millis() ); return Ok(res); } fn build_list_for_group( handouts: &[HandoutEntry], group: &str, req_ctx: &RequestContext, ) -> Markup { let mobile = req_ctx.client_info.device_type == DeviceType::Mobile; if mobile { html! { ul class="handout-ul" { @for h in handouts { @if h.group ==group { li { span class="handout-li-title" { a href=(h.handout) class="underline-link" { strong style="text-decoration: underline;text-underline-offset:1.5pt;color:var(--fgColor);" { (h.title) } } } @if let Some(solutions) = &h.solutions { " [" a href=(solutions) { "sols" } "]" } } } } } } } else { html! { ul class="handout-ul" { @for h in handouts { @if h.group ==group { li { span class="handout-li-title" { strong { (h.title) } } " " span class="handout-li-links" { "[ " @if let Some(solutions) = &h.solutions { a href=(h.handout) {"handout"} " | " a href=(solutions) {"solutions"} } @else { a href=(h.handout) {"handout"} } " ]" } } } } } } } } // // MARK: page // pub fn handouts() -> Page { let md = Markdown::parse(include_str!("handouts.md")); let index = CachedRequest::new( TimeDelta::minutes(30), Box::new(|| Box::pin(async move { get_index().await })), ); tokio::spawn(index.clone().autoget(Duration::from_secs(60 * 20))); #[expect(clippy::unwrap_used)] let mut meta = meta_from_markdown(&md).unwrap().unwrap(); if meta.image.is_none() { meta.image = Some(Image_Icon::URL.to_owned()); } let html = PreEscaped(md.render()); Page { meta, html_ttl: Some(TimeDelta::seconds(300)), generate_html: Box::new(move |page, req_ctx| { let html = html.clone(); // TODO: find a way to not clone here let index = index.clone(); Box::pin(async move { let handouts = index.get().await; let fallback = html! { span style="color:var(--yellow)" { "Could not load handouts, something broke." } " " ( FarLink( "https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest", "Try this direct link." ) ) }; let warmups = match &*handouts { Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", req_ctx), Err(error) => { warn!("Could not load handout index: {error:?}"); fallback.clone() } }; let advanced = match &*handouts { Ok(handouts) => build_list_for_group(handouts, "Advanced", req_ctx), Err(_) => fallback, }; html! { @if let Some(slug) = &page.meta.slug { (Backlinks(&[("/", "home")], slug)) } (html) (Markdown(concat!( "## Warm-Ups", "\n\n", "Students never show up on time. Some come early, some come late. Warm-ups ", "are my solution to this problem: we hand these out as students walk in, ", "giving them something to do until we can start the lesson.", ))) (warmups) br {} (Markdown(concat!( "## Advanced", "\n\n", "The highest level of the ORMC, and the group I spend most of my time with. ", "Students in ORMC Advanced are in high school, which means ", "they're ~14-18 years old.", ))) (advanced) br {} } }) }), } }