diff options
| author | Xe Iaso <me@christine.website> | 2022-11-24 15:46:42 -0500 |
|---|---|---|
| committer | Xe Iaso <me@christine.website> | 2022-11-24 15:46:42 -0500 |
| commit | 18fcf051499912f17dd49bc0413e1de58b2d44f2 (patch) | |
| tree | d16ed58127bfcb8b1b4ee9bdb88fd35cb687c8c9 /src | |
| parent | 551e0384c923ff3ee98cfddf7e3eb42c6dbb2941 (diff) | |
| download | xesite-18fcf051499912f17dd49bc0413e1de58b2d44f2.tar.xz xesite-18fcf051499912f17dd49bc0413e1de58b2d44f2.zip | |
Start version 3
* Change version to 3.0.0 in Cargo.toml
* Add metadata for series
* Change types for signal boosts
* Add start of LaTeX resume generation at Nix time
* Add start of proper author tagging for posts in JSONFeed and ldjson
* Convert templates to use Maud
* Add start of dynamic resume generation from dhall
* Make patrons page embed thumbnails
TODO:
* [ ] Remove the rest of the old templates
* [ ] Bring in Xeact for the share on mastodon button
* [ ] Site update post
Signed-off-by: Xe <me@christine.website>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/config.rs | 90 | ||||
| -rw-r--r-- | src/app/mod.rs | 2 | ||||
| -rw-r--r-- | src/handlers/blog.rs | 55 | ||||
| -rw-r--r-- | src/handlers/gallery.rs | 28 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 75 | ||||
| -rw-r--r-- | src/handlers/talks.rs | 9 | ||||
| -rw-r--r-- | src/main.rs | 12 | ||||
| -rw-r--r-- | src/post/mod.rs | 10 | ||||
| -rw-r--r-- | src/signalboost.rs | 12 | ||||
| -rw-r--r-- | src/tmpl/asciiart.txt | 45 | ||||
| -rw-r--r-- | src/tmpl/mod.rs | 602 | ||||
| -rw-r--r-- | src/tmpl/nag.rs | 2 |
12 files changed, 813 insertions, 129 deletions
diff --git a/src/app/config.rs b/src/app/config.rs index e0a4d90..6499e72 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -1,7 +1,8 @@ use crate::signalboost::Person; -use maud::{html, Markup}; +use maud::{html, Markup, Render}; use serde::{Deserialize, Serialize}; use std::{ + collections::HashMap, fmt::{self, Display}, path::PathBuf, }; @@ -9,7 +10,9 @@ use std::{ #[derive(Clone, Deserialize, Default)] pub struct Config { pub signalboost: Vec<Person>, - pub authors: Vec<Author>, + pub authors: HashMap<String, Author>, + #[serde(rename = "defaultAuthor")] + pub default_author: Author, pub port: u16, #[serde(rename = "clackSet")] pub clack_set: Vec<String>, @@ -19,6 +22,35 @@ pub struct Config { pub mi_token: String, #[serde(rename = "jobHistory")] pub job_history: Vec<Job>, + #[serde(rename = "seriesDescriptions")] + pub series_descriptions: Vec<SeriesDescription>, + #[serde(rename = "seriesDescMap")] + pub series_desc_map: HashMap<String, String>, + #[serde(rename = "notableProjects")] + pub notable_projects: Vec<Link>, + #[serde(rename = "contactLinks")] + pub contact_links: Vec<Link>, +} + +#[derive(Clone, Deserialize, Serialize, Default)] +pub struct Link { + pub url: String, + pub title: String, + pub description: String, +} + +impl Render for Link { + fn render(&self) -> Markup { + html! { + span { + a href=(self.url) {(self.title)} + @if !self.description.is_empty() { + ": " + (self.description) + } + } + } + } } #[derive(Clone, Deserialize, Serialize)] @@ -33,17 +65,51 @@ impl Default for StockKind { } } +fn schema_context() -> String { + "http://schema.org/".to_string() +} + +fn schema_person_type() -> String { + "Person".to_string() +} + #[derive(Clone, Deserialize, Serialize, Default)] pub struct Author { + #[serde(rename = "@context", default = "schema_context")] + pub context: String, + #[serde(rename = "@type", default = "schema_person_type")] + pub schema_type: String, pub name: String, + #[serde(skip_serializing)] pub handle: String, - #[serde(rename = "picUrl")] + #[serde(rename = "image", skip_serializing_if = "Option::is_none")] pub pic_url: Option<String>, - pub link: Option<String>, - pub twitter: Option<String>, - pub default: bool, - #[serde(rename = "inSystem")] + #[serde(rename = "inSystem", skip_serializing)] pub in_system: bool, + #[serde(rename = "jobTitle")] + pub job_title: String, + #[serde(rename = "sameAs")] + pub same_as: Vec<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option<String>, +} + +#[derive(Clone, Deserialize, Serialize, Default)] +pub struct SeriesDescription { + pub name: String, + pub details: String, +} + +impl Render for SeriesDescription { + fn render(&self) -> Markup { + html! { + span { + a href={"/blog/series/" (self.name)} { (self.name) } + ": " + (self.details) + } + } + } } #[derive(Clone, Deserialize, Serialize, Default)] @@ -80,8 +146,8 @@ impl Display for Salary { } } -impl Salary { - pub fn html(&self) -> Markup { +impl Render for Salary { + fn render(&self) -> Markup { if self.stock.is_none() { return html! { (maud::display(self)) }; } @@ -162,15 +228,15 @@ pub struct Company { pub defunct: bool, } -impl Job { - pub fn pay_history_row(&self) -> Markup { +impl Render for Job { + fn render(&self) -> Markup { html! { tr { td { (self.title) } td { (self.start_date) } td { (self.end_date.as_ref().unwrap_or(&"current".to_string())) } td { (if self.days_worked.is_some() { self.days_worked.as_ref().unwrap().to_string() } else { "n/a".to_string() }) } - td { (self.salary.html()) } + td { (self.salary) } td { (self.leave_reason.as_ref().unwrap_or(&"n/a".to_string())) } } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 73320e1..8c7cc4e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -85,7 +85,7 @@ pub async fn init(cfg: PathBuf) -> Result<State> { everything.sort(); everything.reverse(); - let today = Utc::today(); + let today = Utc::now().date_naive(); let everything: Vec<Post> = everything .into_iter() .filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce()) diff --git a/src/handlers/blog.rs b/src/handlers/blog.rs index 012a7ad..09cef8b 100644 --- a/src/handlers/blog.rs +++ b/src/handlers/blog.rs @@ -1,14 +1,16 @@ use super::Result; -use crate::{app::State, post::Post, templates}; +use crate::{app::State, post::Post, templates, tmpl}; use axum::{ extract::{Extension, Path}, + http::StatusCode, response::Html, }; use http::HeaderMap; use lazy_static::lazy_static; +use maud::Markup; use prometheus::{opts, register_int_counter_vec, IntCounterVec}; use std::sync::Arc; -use tracing::{error, instrument}; +use tracing::instrument; lazy_static! { static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!( @@ -19,40 +21,27 @@ lazy_static! { } #[instrument(skip(state))] -pub async fn index(Extension(state): Extension<Arc<State>>) -> Result { +pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> { let state = state.clone(); - let mut result: Vec<u8> = vec![]; - templates::blogindex_html(&mut result, state.blog.clone())?; - Ok(Html(result)) + let result = tmpl::post_index(&state.blog, "Blogposts", true); + Ok(result) } #[instrument(skip(state))] -pub async fn series(Extension(state): Extension<Arc<State>>) -> Result { +pub async fn series(Extension(state): Extension<Arc<State>>) -> Result<Markup> { let state = state.clone(); - let mut series: Vec<String> = vec![]; - let mut result: Vec<u8> = vec![]; - for post in &state.blog { - if post.front_matter.series.is_some() { - series.push(post.front_matter.series.as_ref().unwrap().clone()); - } - } - - series.sort(); - series.dedup(); - - templates::series_html(&mut result, series)?; - Ok(Html(result)) + Ok(tmpl::blog_series(&state.cfg.clone().series_descriptions)) } #[instrument(skip(state))] pub async fn series_view( Path(series): Path<String>, Extension(state): Extension<Arc<State>>, -) -> Result { +) -> (StatusCode, Markup) { let state = state.clone(); + let cfg = state.cfg.clone(); let mut posts: Vec<Post> = vec![]; - let mut result: Vec<u8> = vec![]; for post in &state.blog { if post.front_matter.series.is_none() { @@ -64,13 +53,25 @@ pub async fn series_view( posts.push(post.clone()); } + posts.reverse(); + + let desc = cfg.series_desc_map.get(&series); + if posts.len() == 0 { - error!("series not found"); - return Err(super::Error::SeriesNotFound(series)); + ( + StatusCode::NOT_FOUND, + tmpl::error(format!("series not found: {series}")), + ) + } else { + if let Some(desc) = desc { + (StatusCode::OK, tmpl::series_view(&series, desc, &posts)) + } else { + ( + StatusCode::INTERNAL_SERVER_ERROR, + tmpl::error(format!("series metadata in dhall not found: {series}")), + ) + } } - - templates::series_posts_html(&mut result, series, &posts).unwrap(); - Ok(Html(result)) } #[instrument(skip(state, headers))] diff --git a/src/handlers/gallery.rs b/src/handlers/gallery.rs index ae6c411..fd3ed1b 100644 --- a/src/handlers/gallery.rs +++ b/src/handlers/gallery.rs @@ -1,10 +1,8 @@ -use super::{Error::*, Result}; -use crate::{app::State, post::Post, templates}; -use axum::{ - extract::{Extension, Path}, - response::Html, -}; +use crate::{app::State, post::Post, tmpl}; +use axum::extract::{Extension, Path}; +use http::StatusCode; use lazy_static::lazy_static; +use maud::Markup; use prometheus::{opts, register_int_counter_vec, IntCounterVec}; use std::sync::Arc; use tracing::instrument; @@ -18,36 +16,32 @@ lazy_static! { } #[instrument(skip(state))] -pub async fn index(Extension(state): Extension<Arc<State>>) -> Result { +pub async fn index(Extension(state): Extension<Arc<State>>) -> Markup { let state = state.clone(); - let mut result: Vec<u8> = vec![]; - templates::galleryindex_html(&mut result, state.gallery.clone())?; - Ok(Html(result)) + tmpl::gallery_index(&state.gallery) } #[instrument(skip(state))] pub async fn post_view( Path(name): Path<String>, Extension(state): Extension<Arc<State>>, -) -> Result { +) -> (StatusCode, Markup) { let mut want: Option<Post> = None; + let link = format!("gallery/{}", name); for post in &state.gallery { - if post.link == format!("gallery/{}", name) { + if post.link == link { want = Some(post.clone()); } } match want { - None => Err(PostNotFound(name)), + None => (StatusCode::NOT_FOUND, tmpl::not_found(link)), Some(post) => { HIT_COUNTER .with_label_values(&[name.clone().as_str()]) .inc(); - let body = templates::Html(post.body_html.clone()); - let mut result: Vec<u8> = vec![]; - templates::gallerypost_html(&mut result, post, body)?; - Ok(Html(result)) + (StatusCode::OK, tmpl::gallery_post(&post)) } } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 37fcac1..0bcebe7 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,4 +1,4 @@ -use crate::{app::State, templates}; +use crate::{app::State, tmpl}; use axum::{ body, extract::Extension, @@ -7,6 +7,7 @@ use axum::{ }; use chrono::{Datelike, Timelike, Utc, Weekday}; use lazy_static::lazy_static; +use maud::{Markup, PreEscaped}; use prometheus::{opts, register_int_counter_vec, IntCounterVec}; use std::sync::Arc; use tracing::instrument; @@ -67,82 +68,76 @@ lazy_static! { }; } -#[instrument] -pub async fn index() -> Result { +#[instrument(skip(state))] +pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> { HIT_COUNTER.with_label_values(&["index"]).inc(); - let mut result: Vec<u8> = vec![]; - templates::index_html(&mut result)?; - Ok(Html(result)) + let state = state.clone(); + let cfg = state.cfg.clone(); + + Ok(tmpl::index(&cfg.default_author, &cfg.notable_projects)) } -#[instrument] -pub async fn contact() -> Result { +#[instrument(skip(state))] +pub async fn contact(Extension(state): Extension<Arc<State>>) -> Markup { HIT_COUNTER.with_label_values(&["contact"]).inc(); - let mut result: Vec<u8> = vec![]; - templates::contact_html(&mut result)?; - Ok(Html(result)) + let state = state.clone(); + let cfg = state.cfg.clone(); + + crate::tmpl::contact(&cfg.contact_links) } #[instrument] -pub async fn feeds() -> Result { +pub async fn feeds() -> Markup { HIT_COUNTER.with_label_values(&["feeds"]).inc(); - let mut result: Vec<u8> = vec![]; - templates::feeds_html(&mut result)?; - Ok(Html(result)) + crate::tmpl::feeds() } #[axum_macros::debug_handler] #[instrument(skip(state))] -pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result { +pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result<Markup> { HIT_COUNTER .with_label_values(&["salary_transparency"]) .inc(); let state = state.clone(); - let mut result: Vec<u8> = vec![]; - templates::salary_transparency(&mut result, state.cfg.clone())?; - Ok(Html(result)) + let cfg = state.cfg.clone(); + + Ok(tmpl::salary_transparency(&cfg.job_history)) } #[axum_macros::debug_handler] #[instrument(skip(state))] -pub async fn resume(Extension(state): Extension<Arc<State>>) -> Result { +pub async fn resume(Extension(state): Extension<Arc<State>>) -> Result<Markup> { HIT_COUNTER.with_label_values(&["resume"]).inc(); let state = state.clone(); - let mut result: Vec<u8> = vec![]; - templates::resume_html(&mut result, templates::Html(state.resume.clone()))?; - Ok(Html(result)) + + Ok(tmpl::resume(PreEscaped(&state.resume))) } #[instrument(skip(state))] -pub async fn patrons(Extension(state): Extension<Arc<State>>) -> Result { +pub async fn patrons(Extension(state): Extension<Arc<State>>) -> (StatusCode, Markup) { HIT_COUNTER.with_label_values(&["patrons"]).inc(); let state = state.clone(); - let mut result: Vec<u8> = vec![]; match &state.patrons { - None => Err(Error::NoPatrons), - Some(patrons) => { - templates::patrons_html(&mut result, patrons.clone())?; - Ok(Html(result)) - } + None => ( + StatusCode::INTERNAL_SERVER_ERROR, + tmpl::error("Patreon API config is broken, no patrons in ram"), + ), + Some(patrons) => (StatusCode::IM_A_TEAPOT, tmpl::patrons(&patrons)), } } #[axum_macros::debug_handler] #[instrument(skip(state))] -pub async fn signalboost(Extension(state): Extension<Arc<State>>) -> Result { +pub async fn signalboost(Extension(state): Extension<Arc<State>>) -> Markup { HIT_COUNTER.with_label_values(&["signalboost"]).inc(); let state = state.clone(); - let mut result: Vec<u8> = vec![]; - templates::signalboost_html(&mut result, state.signalboost.clone())?; - Ok(Html(result)) + tmpl::signalboost(&state.signalboost) } #[instrument] -pub async fn not_found() -> Result { +pub async fn not_found(uri: axum::http::Uri) -> (StatusCode, Markup) { HIT_COUNTER.with_label_values(&["not_found"]).inc(); - let mut result: Vec<u8> = vec![]; - templates::notfound_html(&mut result, "some path".into())?; - Ok(Html(result)) + (StatusCode::NOT_FOUND, tmpl::not_found(uri.path())) } #[derive(Debug, thiserror::Error)] @@ -170,8 +165,8 @@ pub type Result<T = Html<Vec<u8>>> = std::result::Result<T, Error>; impl IntoResponse for Error { fn into_response(self) -> Response { - let mut result: Vec<u8> = vec![]; - templates::error_html(&mut result, format!("{}", self)).unwrap(); + let result = tmpl::error(format!("{}", self)); + let result = result.0; let body = body::boxed(body::Full::from(result)); diff --git a/src/handlers/talks.rs b/src/handlers/talks.rs index 59d8676..1b27a16 100644 --- a/src/handlers/talks.rs +++ b/src/handlers/talks.rs @@ -1,11 +1,12 @@ use super::{Error::*, Result}; -use crate::{app::State, post::Post, templates}; +use crate::{app::State, post::Post, templates, tmpl}; use axum::{ extract::{Extension, Path}, response::Html, }; use http::header::HeaderMap; use lazy_static::lazy_static; +use maud::Markup; use prometheus::{opts, register_int_counter_vec, IntCounterVec}; use std::sync::Arc; use tracing::instrument; @@ -19,11 +20,9 @@ lazy_static! { } #[instrument(skip(state))] -pub async fn index(Extension(state): Extension<Arc<State>>) -> Result { +pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Markup> { let state = state.clone(); - let mut result: Vec<u8> = vec![]; - templates::talkindex_html(&mut result, state.talks.clone())?; - Ok(Html(result)) + Ok(tmpl::post_index(&state.talks, "Talks", false)) } #[instrument(skip(state, headers))] diff --git a/src/main.rs b/src/main.rs index a35db42..737ab45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,9 @@ extern crate tracing; use axum::{ body, extract::Extension, + handler::Handler, http::header::{self, HeaderValue, CONTENT_TYPE}, - response::{Html, Response}, + response::Response, routing::{get, get_service}, Router, }; @@ -211,6 +212,7 @@ async fn main() -> Result<()> { ) }), ) + .fallback(handlers::not_found.into_service()) .layer(middleware); #[cfg(target_os = "linux")] @@ -276,16 +278,12 @@ async fn metrics() -> Response { .unwrap() } -async fn go_vanity() -> Html<Vec<u8>> { - let mut buffer: Vec<u8> = vec![]; - templates::gitea_html( - &mut buffer, +async fn go_vanity() -> maud::Markup { + tmpl::gitea( "christine.website/jsonfeed", "https://tulpa.dev/Xe/jsonfeed", "master", ) - .unwrap(); - Html(buffer) } include!(concat!(env!("OUT_DIR"), "/templates.rs")); diff --git a/src/post/mod.rs b/src/post/mod.rs index 8ed99bf..2046a89 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -99,10 +99,12 @@ async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Resul let link = format!("{}/{}", dir, fname.file_stem().unwrap().to_str().unwrap()); let body_html = xesite_markdown::render(&body) .wrap_err_with(|| format!("can't parse markdown for {:?}", fname))?; - let date: DateTime<FixedOffset> = - DateTime::<Utc>::from_utc(NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), Utc) - .with_timezone(&Utc) - .into(); + let date: DateTime<FixedOffset> = DateTime::<Utc>::from_utc( + NaiveDateTime::new(date, NaiveTime::from_hms_opt(0, 0, 0).unwrap()), + Utc, + ) + .with_timezone(&Utc) + .into(); let mentions: Vec<mi::WebMention> = match cli { Some(cli) => cli diff --git a/src/signalboost.rs b/src/signalboost.rs index 6adfc8f..d4638d9 100644 --- a/src/signalboost.rs +++ b/src/signalboost.rs @@ -1,17 +1,11 @@ +use crate::app::config::Link; use serde::Deserialize; -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Deserialize)] pub struct Person { pub name: String, pub tags: Vec<String>, - #[serde(rename = "gitLink")] - pub git_link: Option<String>, - pub twitter: Option<String>, - pub linkedin: Option<String>, - pub fediverse: Option<String>, - #[serde(rename = "coverLetter")] - pub cover_letter: Option<String>, - pub website: Option<String>, + pub links: Vec<Link>, } #[cfg(test)] diff --git a/src/tmpl/asciiart.txt b/src/tmpl/asciiart.txt new file mode 100644 index 0000000..88fe61e --- /dev/null +++ b/src/tmpl/asciiart.txt @@ -0,0 +1,45 @@ +<!-- +MMMMMMMMMMMMMMMMMMNmmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmmd.:mmMM +MMMMMMMMMMMMMMMMMNmmmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmydmmmmmNMM +MMMMMMMMMMMMMMMMNm/:mNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmms /mmmmmMMM +MMMMMMMMMMMMMMMNmm:-dmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmmmdsdmmmmNMMM +MMMMMMMMMMMMMMMmmmmmmmNMMMMMMMMMMMNmmdhhddhhmNNMMMMMMMMMMMMMMMMNmy:hmmmmmmmmMMMM +MMMMMMMMMMMMMMNm++mmmmNMMMMMMmdyo/::.........-:/sdNMMMMMMMMMMNmmms`smmmmmmmNMMMM +MMMMMMMMMMMMMMmd.-dmmmmMMmhs/-....................-+dNMMMMMMNmmmmmmmmmmmmmmMMMMM +MMMMMMMMMMMMMNmmmmmmmmho:-...........................:sNMMNmmmmmmmmmmmmmmmNMNmdd +MMMMMMMMMMMMNmd+ydhs/-.................................-sNmmmmmmmmmmmmmmmdhyssss +MMMMMMMMMMMNNh+`........................................:dmmmmmmmmmmmmmmmyssssss +MMMMNNdhy+:-...........................................+dmmmmmmmmmmmmmmmdsssssss +MMMN+-...............................................-smmmmmmmmmmmmmmmmmysyyhdmN +MMMMNho:::-.--::-.......................----------..:hmmmmmmmmmmmmmmmmmmmNMMMMMM +MMMMMMMMNNNmmdo:......................--------------:ymmmmmmmmmmmmmmmmmmmMMMMMMM +MMMMMMMMMMds+........................-----------------+dmmmmmmmmmmmmmmmmmMMMMMMM +MMMMMMMMMh+........................--------------------:smmmmmmmmmmmmmmNMMMMMMMM +MMMMMMMNy/........................-------------::--------/hmmmmmmmmmmmNMMMMMMNmd +MMMMMMMd/........................--------------so----------odmmmmmmmmMMNmdhhysss +MMMMMMm/........................--------------+mh-----------:ymmmmdhhyysssssssss +MMMMMMo.......................---------------:dmmo------------+dmdysssssssssssss +yhdmNh:......................---------------:dmmmm+------------:sssssssssssyhhdm +sssssy.......................--------------:hmmmmmmos++:---------/sssyyhdmNMMMMM +ssssso......................--------------:hmmmNNNMNdddysso:------:yNNMMMMMMMMMM +ysssss.....................--------------/dmNyy/mMMd``d/------------sNMMMMMMMMMM +MNmdhy-...................--------------ommmh`o/NM/. smh+-----------:yNMMMMMMMMM +MMMMMN+...................------------/hmmss: `-//-.smmmmd+----------:hMMMMMMMMM +MMMMMMd:..................----------:smmmmhy+oosyysdmmy+:. `.--------/dMMMMMMMM +MMMMMMMh-................---------:smmmmmmmmmmmmmmmh/` `/s:-------sMMMMMMMM +MMMMMMMms:...............-------/ymmmmmmmmmmmmmmmd/ :dMMNy/-----+mMMMMMMM +MMMMMMmyss/..............------ommmmmmmmmmmmmmmmd. :yMMMMMMNs:---+mMMMMMMM +MMMMNdssssso-............----..odmmmmmmmmmmmmmmh:.` .sNMMMMMMMMMd/--sMMMMMMMM +MMMmysssssssh/................` -odmmmmmmmmmh+. `omMMMMMMMMMMMMh/+mMMMMMMMM +MNdyssssssymMNy-.............. `/sssso+:. `+mMMMMMMMMMMMMMMMdNMMMMMMMMM +NhssssssshNMMMMNo:............/.` `+dMMMMMMMMMMMMMMMMMMMMMMMMMMMM +ysssssssdMMMMMMMMm+-..........+ddy/.` -omMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +ssssssymMMMMMMMMMMMh/.........-oNMMNmy+--` `-+dNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +ssssydNMMMMMMMMMMMMMNy:........-hMMMMMMMNmdmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +sssymMMMMMMMMMMMMMMMMMm+....-..:hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +symNMMMMMMMMMMMMMMMMMMMNo.../-/dMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +dNMMMMMMMMMMMMMMMMMMMMMMh:.:hyNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +la budza pu cusku lu + <<.i do snura .i ko kanro + .i do panpi .i ko gleki>> li'u +--> diff --git a/src/tmpl/mod.rs b/src/tmpl/mod.rs index b2ffd0d..9535be2 100644 --- a/src/tmpl/mod.rs +++ b/src/tmpl/mod.rs @@ -1,10 +1,600 @@ -use crate::app::Config; -use maud::{html, Markup}; -use std::sync::Arc; +use crate::{app::*, post::Post, signalboost::Person}; +use chrono::prelude::*; +use lazy_static::lazy_static; +use maud::{html, Markup, PreEscaped, Render, DOCTYPE}; +use patreon::Users; pub mod nag; -pub fn salary_history(cfg: Arc<Config>) -> Markup { +lazy_static! { + static ref CACHEBUSTER: String = uuid::Uuid::new_v4().to_string().replace("-", ""); +} + +pub fn base(title: Option<&str>, styles: Option<&str>, content: Markup) -> Markup { + let now = Utc::now(); + html! { + (DOCTYPE) + (PreEscaped(include_str!("./asciiart.txt"))) + html lang="en" { + head { + title { + @if let Some(title) = title { + (title) + " - Xe Iaso" + } @else { + "Xe Iaso" + } + } + meta name="viewport" content="width=device-width, initial-scale=1.0"; + link rel="stylesheet" href={"/css/hack.css?bustCache=" (*CACHEBUSTER)}; + link rel="stylesheet" href={"/css/gruvbox-dark.css?bustCache=" (*CACHEBUSTER)}; + link rel="stylesheet" href={"/css/shim.css?bustCache=" (*CACHEBUSTER)}; + @match now.month() { + 12|1|2 => { + link rel="stylesheet" href={"/css/snow.css?bustCache=" (*CACHEBUSTER)}; + } + _ => {}, + } + link rel="manifest" href="/static/manifest.json"; + link rel="alternate" title="Xe's Blog" type="application/rss+xml" href="https://xeiaso.net/blog.rss"; + link rel="alternate" title="Xe's Blog" type="application/json" href="https://xeiaso.net/blog.json"; + link rel="apple-touch-icon" sizes="57x57" href="/static/favicon/apple-icon-57x57.png"; + link rel="apple-touch-icon" sizes="60x60" href="/static/favicon/apple-icon-60x60.png"; + link rel="apple-touch-icon" sizes="72x72" href="/static/favicon/apple-icon-72x72.png"; + link rel="apple-touch-icon" sizes="76x76" href="/static/favicon/apple-icon-76x76.png"; + link rel="apple-touch-icon" sizes="114x114" href="/static/favicon/apple-icon-114x114.png"; + link rel="apple-touch-icon" sizes="120x120" href="/static/favicon/apple-icon-120x120.png"; + link rel="apple-touch-icon" sizes="144x144" href="/static/favicon/apple-icon-144x144.png"; + link rel="apple-touch-icon" sizes="152x152" href="/static/favicon/apple-icon-152x152.png"; + link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-icon-180x180.png"; + link rel="icon" type="image/png" sizes="192x192" href="/static/favicon/android-icon-192x192.png"; + link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png"; + link rel="icon" type="image/png" sizes="32x32" href="/static/favicon/favicon-32x32.png"; + link rel="icon" type="image/png" sizes="96x96" href="/static/favicon/favicon-96x96.png"; + link rel="icon" type="image/png" sizes="16x16" href="/static/favicon/favicon-16x16.png"; + meta name="msapplication-TileColor" content="#ffffff"; + meta name="msapplication-TileImage" content="/static/favicon/ms-icon-144x144.png"; + meta name="theme-color" content="#ffffff"; + link href="https://mi.within.website/api/webmention/accept" rel="webmention"; + @if let Some(styles) = styles { + style { + (PreEscaped(styles)) + } + } + } + body.snow.hack.gruvbox-dark { + .container { + header { + span.logo {} + nav { + a href="/" { "Xe" } + " - " + a href="/blog" { "Blog" } + " - " + a href="/contact" { "Contact" } + " - " + a href="/resume" { "Resume" } + " - " + a href="/talks" { "Talks" } + " - " + a href="/signalboost" { "Signal Boost" } + " - " + a href="/feeds" { "Feeds" } + " | " + a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website" { "Graphviz" } + " - " + a target="_blank" rel="noopener noreferrer" href="https://when-then-zen.christine.website/" { "When Then Zen" } + } + } + + br; + br; + + .snowframe { + (content) + } + hr; + footer { + blockquote { + "Copyright 2012-2022 Xe Iaso (Christine Dodrill). Any and all opinions listed here are my own and not representative of my employers; future, past and present." + } + p { + "Like what you see? Donate on " + a href="https://www.patreon.com/cadey" { "Patreon" } + " like " + a href="/patrons" { "these awesome people" } + "!" + } + p { + "Looking for someone for your team? Take a look " + a href="/signalboost" { "here" } + "." + } + p { + "See my salary transparency data " + a href="/salary-transparency" {"here"} + "." + } + p { + "Served by " + (env!("out")) + "/bin/xesite, see " + a href="https://github.com/Xe/site" { "source code here" } + "." + } + } + script src="/static/js/installsw.js" defer {} + } + } + } + } +} + +pub fn post_index(posts: &Vec<Post>, title: &str, show_extra: bool) -> Markup { + let today = Utc::now().date_naive(); + base( + Some(title), + None, + html! { + h1 { (title) } + @if show_extra { + p { + "If you have a compatible reader, be sure to check out my " + a href="/blog.rss" { "RSS feed" } + " for automatic updates. Also check out the " + a href="/blog.json" { "JSONFeed" } + "." + } + p { + "For a breakdown by post series, see " + a href="/blog/series" { "here" } + "." + } + } + p { + ul { + @for post in posts.iter().filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce()) { + li { + (post.detri()) + " - " + a href={"/" (post.link)} { (post.front_matter.title) } + } + } + } + } + }, + ) +} + +pub fn gallery_index(posts: &Vec<Post>) -> Markup { + base( + Some("Gallery"), + None, + html! { + h1 {"Gallery"} + + p {"Here are links to a lot of the art I have done in the last few years."} + + .grid { + @for post in posts { + .card.cell."-4of12".blogpost-card { + header."card-header" { + (post.front_matter.title) + } + .card-content { + center { + p { + "Posted on " + (post.detri()) + br; + a href={"/" (post.link)} { + img src=(post.front_matter.thumb.as_ref().unwrap()); + } + } + } + } + } + } + } + }, + ) +} + +pub fn gallery_post(post: &Post) -> Markup { + base |
