use crate::page; use anyhow::Result; use maud::{html, Markup, PreEscaped}; use serde::Deserialize; use toml::value::Date; impl page::Page for Vec { fn page_title() -> String { String::from("Stories") } fn render_content(&self) -> Markup { html!( section class="stories" { div class="stories-filter" { label for="all" { input type="radio" id="all" name="language" checked; "All" } label for="de" { input type="radio" id="de" name="language"; "German" } label for="en" { input type="radio" id="en" name="language"; "English" } } @for story in self { (story.render_as_card()) } } ) } fn prepare_content(&mut self) -> Result<()> { self.sort_by(|a, b| { let date1 = a.update_date.unwrap_or(a.upload_date); let date2 = b.update_date.unwrap_or(b.upload_date); date2.cmp(&date1) }); for story in self { std::fs::copy(&story.path, story.format_local_pdf_path())?; } Ok(()) } fn content_style() -> Markup { html!( (PreEscaped( r#" h2 { font-size: 1.8rem; margin-bottom: 2rem; color: #2F3C57; } hr { margin: 1rem; } .story-card { text-align: left; font-size: 1.2rem; hyphens: auto; ul { display: flex; justify-content: start; list-style: none; flex-wrap: wrap; li { font-size: 1rem; padding: 0.8rem; margin: 0.8rem; background-color: #2F3C57; color: #ffd1a3; outline: 0.2rem solid #2F3C57; a { text-decoration: underline #E1CCEB; color: #E1CCEB; } } } hr { margin: 2rem 0; } p { margin-top: 1rem; } padding: 3rem; margin: 3rem; background-color: #ffd1a3; border-bottom: 0.1rem dotted black; .content-warning { background-color: #EC7981; margin: 2rem 0; padding: 0.3rem; width: fit-content; } } .stories:has(input:checked) .story-card { display: none; } .stories:has(#all:checked) .story-card, .stories:has(#de:checked) .story-card[lang="de"], .stories:has(#en:checked) .story-card[lang="en"] { display: block; } .stories-filter { display: flex; align-items: center; border: .1rem dotted #000; margin: 3rem; width: fit-content; border:.1rem dotted #000; label { margin: 0.8rem; padding: .8rem; } input { margin-right: 0.5rem; appearance: none; } label:hover { cursor: pointer; color: #11396A; } label:has(input:checked) { color: #FFD1A3; background-color: #11396A; #background-color: #2F3C57; } } "# )) ) } } #[derive(Deserialize, Debug)] pub enum StoryLanguage { German, English, } impl std::fmt::Display for StoryLanguage { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } #[derive(Deserialize)] pub struct Story { pub name: String, pub description: String, pub language: StoryLanguage, pub upload_date: Date, pub update_date: Option, pub path: String, pub warnings: Option>, } impl Story { fn render_as_card(&self) -> Markup { html!(div class="story-card" lang=(self.html_lang_value()) { card { h2 { (self.name) } @if let Some(content_warnings) = &self.render_content_warnings() { (content_warnings) } p { (self.description) } hr {} ul lang="en" { li { "Language: " (self.language) } li { "Uploaded on: " (self.upload_date) } @if let Some(update_date) = self.update_date { li { "Updated on: " (update_date) } } li { a href=(&self.format_pdf_path()) download { "PDF" } } } } }) } pub fn format_pdf_path(&self) -> String { let snake_case_name = self.name.to_lowercase().replace(" ", "_"); format!("stories/{}.pdf", snake_case_name) } fn format_local_pdf_path(&self) -> String { format!("dist/{}", self.format_pdf_path()) } pub fn html_lang_value(&self) -> String { match self.language { StoryLanguage::German => String::from("de"), StoryLanguage::English => String::from("en"), } } pub fn render_content_warnings(&self) -> Option { if let Some(warnings) = &self.warnings { let (summary, prefix) = match self.language { StoryLanguage::German => ( String::from("Vorsicht ist geboten."), String::from("Die Geschichte beinhaltet: "), ), StoryLanguage::English => ( String::from("Reader discretion is advised."), String::from("The story contains: "), ), }; let details = format!("{}{}", prefix, warnings.join(", ")); Some(html!( details { summary { strong { (summary) } } p class="content-warning" { (details) } } )) } else { None } } }