diff options
| author | Xe Iaso <me@christine.website> | 2022-06-14 15:04:17 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-14 15:04:17 -0400 |
| commit | ad6fba4c79e8b5ab08e2f0db8bc4087f03151f7f (patch) | |
| tree | 9888fe24eb3ea35ea0f9b54af8723b4a000e6ad9 /src | |
| parent | 7541df778165b5a96da714256d011685b476abc0 (diff) | |
| download | xesite-ad6fba4c79e8b5ab08e2f0db8bc4087f03151f7f.tar.xz xesite-ad6fba4c79e8b5ab08e2f0db8bc4087f03151f7f.zip | |
Add salary transparency page (#492)
* Move dhall data and types into `/dhall` folder
* Reformat salary transparency data into Dhall
* Wire up the old salary transparency page with a custom element
* Wire up a new salary transparency page
* Expose raw data as JSON
* Make dhall types more portable
* Remove gallery from the navbar
* Make signal boost page point to the new data location
* Add salary transparency page to the footer of the site
* Add site update post for this
Signed-off-by: Xe <me@xeiaso.net>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/markdown.rs | 9 | ||||
| -rw-r--r-- | src/app/mod.rs | 77 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 28 | ||||
| -rw-r--r-- | src/main.rs | 6 | ||||
| -rw-r--r-- | src/post/mod.rs | 27 | ||||
| -rw-r--r-- | src/signalboost.rs | 3 | ||||
| -rw-r--r-- | src/tmpl/mod.rs | 20 |
7 files changed, 147 insertions, 23 deletions
diff --git a/src/app/markdown.rs b/src/app/markdown.rs index f6ae342..d73a5c5 100644 --- a/src/app/markdown.rs +++ b/src/app/markdown.rs @@ -1,3 +1,4 @@ +use crate::app::Config; use crate::templates::Html; use color_eyre::eyre::{Result, WrapErr}; use comrak::nodes::{Ast, AstNode, NodeValue}; @@ -9,13 +10,14 @@ use comrak::{ use lazy_static::lazy_static; use lol_html::{element, html_content::ContentType, rewrite_str, RewriteStrSettings}; use std::cell::RefCell; +use std::sync::Arc; use url::Url; lazy_static! { static ref SYNTECT_ADAPTER: SyntectAdapter<'static> = SyntectAdapter::new("base16-mocha.dark"); } -pub fn render(inp: &str) -> Result<String> { +pub fn render(cfg: Arc<Config>, inp: &str) -> Result<String> { let mut options = ComrakOptions::default(); options.extension.autolink = true; @@ -100,6 +102,11 @@ pub fn render(inp: &str) -> Result<String> { let file = el.get_attribute("file").expect("wanted xeblog-hero to contain file"); el.replace(&crate::tmpl::xeblog_hero(file, el.get_attribute("prompt")).0, ContentType::Html); Ok(()) + }), + element!("xeblog-salary-history", |el| { + el.replace(&crate::tmpl::xeblog_salary_history(cfg.clone()).0, ContentType::Html); + + Ok(()) }) ], ..RewriteStrSettings::default() diff --git a/src/app/mod.rs b/src/app/mod.rs index 7125cf8..a12d1c6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,23 +1,73 @@ use crate::{post::Post, signalboost::Person}; -use color_eyre::eyre::Result; use chrono::prelude::*; -use serde::Deserialize; +use color_eyre::eyre::Result; +use maud::{html, Markup}; +use serde::{Deserialize, Serialize}; use std::{ + fmt::{self, Display}, fs, path::PathBuf, + sync::Arc, }; use tracing::{error, instrument}; pub mod markdown; pub mod poke; -#[derive(Clone, Deserialize)] +#[derive(Clone, Deserialize, Default)] pub struct Config { pub(crate) signalboost: Vec<Person>, #[serde(rename = "resumeFname")] pub(crate) resume_fname: PathBuf, #[serde(rename = "miToken")] pub(crate) mi_token: String, + #[serde(rename = "jobHistory")] + pub(crate) job_history: Vec<Job>, +} + +#[derive(Clone, Deserialize, Serialize, Default)] +pub struct Salary { + pub amount: i32, + pub per: String, + pub currency: String, +} + +impl Display for Salary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}${}/{}", self.currency, self.amount, self.per) + } +} + +#[derive(Clone, Deserialize, Serialize, Default)] +pub struct Job { + pub company: String, + pub title: String, + #[serde(rename = "startDate")] + pub start_date: String, + #[serde(rename = "endDate")] + pub end_date: Option<String>, + #[serde(rename = "daysWorked")] + pub days_worked: Option<i32>, + #[serde(rename = "daysBetween")] + pub days_between: Option<i32>, + pub salary: Salary, + #[serde(rename = "leaveReason")] + pub leave_reason: Option<String>, +} + +impl Job { + pub fn pay_history_row(&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) } + td { (self.leave_reason.as_ref().unwrap_or(&"n/a".to_string())) } + } + } + } } #[instrument] @@ -57,7 +107,7 @@ async fn patrons() -> Result<Option<patreon::Users>> { pub const ICON: &'static str = "https://xeiaso.net/static/img/avatar.png"; pub struct State { - pub cfg: Config, + pub cfg: Arc<Config>, pub signalboost: Vec<Person>, pub resume: String, pub blog: Vec<Post>, @@ -71,14 +121,17 @@ pub struct State { } pub async fn init(cfg: PathBuf) -> Result<State> { - let cfg: Config = serde_dhall::from_file(cfg).parse()?; + let cfg: Arc<Config> = Arc::new(serde_dhall::from_file(cfg).parse()?); let sb = cfg.signalboost.clone(); - let resume = fs::read_to_string(cfg.resume_fname.clone())?; - let resume: String = markdown::render(&resume)?; - let mi = mi::Client::new(cfg.mi_token.clone(), crate::APPLICATION_NAME.to_string())?; - let blog = crate::post::load("blog").await?; - let gallery = crate::post::load("gallery").await?; - let talks = crate::post::load("talks").await?; + let resume = fs::read_to_string(cfg.clone().resume_fname.clone())?; + let resume: String = markdown::render(cfg.clone(), &resume)?; + let mi = mi::Client::new( + cfg.clone().mi_token.clone(), + crate::APPLICATION_NAME.to_string(), + )?; + let blog = crate::post::load(cfg.clone(), "blog").await?; + let gallery = crate::post::load(cfg.clone(), "gallery").await?; + let talks = crate::post::load(cfg.clone(), "talks").await?; let mut everything: Vec<Post> = vec![]; { @@ -99,7 +152,7 @@ pub async fn init(cfg: PathBuf) -> Result<State> { .filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce()) .take(5) .collect(); - + let mut jfb = jsonfeed::Feed::builder() .title("Xe's Blog") .description("My blog posts and rants about various technology things.") diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index fa8203c..fc2a154 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,9 +1,13 @@ -use crate::{app::State, templates}; +use crate::{ + app::{Job, State}, + templates, +}; use axum::{ body, extract::Extension, http::StatusCode, response::{Html, IntoResponse, Response}, + Json, }; use chrono::{Datelike, Timelike, Utc, Weekday}; use lazy_static::lazy_static; @@ -74,6 +78,28 @@ pub async fn feeds() -> Result { #[axum_macros::debug_handler] #[instrument(skip(state))] +pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result { + 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)) +} + +#[axum_macros::debug_handler] +#[instrument(skip(state))] +pub async fn salary_transparency_json(Extension(state): Extension<Arc<State>>) -> Json<Vec<Job>> { + HIT_COUNTER + .with_label_values(&["salary_transparency_json"]) + .inc(); + + Json(state.clone().cfg.clone().job_history.clone()) +} + +#[axum_macros::debug_handler] +#[instrument(skip(state))] pub async fn resume(Extension(state): Extension<Arc<State>>) -> Result { HIT_COUNTER.with_label_values(&["resume"]).inc(); let state = state.clone(); diff --git a/src/main.rs b/src/main.rs index 7611176..eb4b9e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -155,6 +155,11 @@ async fn main() -> Result<()> { }, ), ) + // api + .route( + "/api/salary_transparency.json", + get(handlers::salary_transparency_json), + ) // static pages .route("/", get(handlers::index)) .route("/contact", get(handlers::contact)) @@ -162,6 +167,7 @@ async fn main() -> Result<()> { .route("/resume", get(handlers::resume)) .route("/patrons", get(handlers::patrons)) .route("/signalboost", get(handlers::signalboost)) + .route("/salary-transparency", get(handlers::salary_transparency)) // feeds .route("/blog.json", get(handlers::feeds::jsonfeed)) .route("/blog.atom", get(handlers::feeds::atom)) diff --git a/src/post/mod.rs b/src/post/mod.rs index 3e4cb8a..96c3e73 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -1,8 +1,9 @@ +use crate::app::Config; use chrono::prelude::*; use color_eyre::eyre::{eyre, Result, WrapErr}; use glob::glob; use serde::{Deserialize, Serialize}; -use std::{borrow::Borrow, cmp::Ordering, path::PathBuf}; +use std::{borrow::Borrow, cmp::Ordering, path::PathBuf, sync::Arc}; use tokio::fs; pub mod frontmatter; @@ -81,7 +82,12 @@ impl Post { } } -async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Result<Post> { +async fn read_post( + cfg: Arc<Config>, + dir: &str, + fname: PathBuf, + cli: &Option<mi::Client>, +) -> Result<Post> { debug!( "loading {}", fname.clone().into_os_string().into_string().unwrap() @@ -96,7 +102,7 @@ async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Resul let date = NaiveDate::parse_from_str(&front_matter.clone().date, "%Y-%m-%d") .map_err(|why| eyre!("error parsing date in {:?}: {}", fname, why))?; let link = format!("{}/{}", dir, fname.file_stem().unwrap().to_str().unwrap()); - let body_html = crate::app::markdown::render(&body) + let body_html = crate::app::markdown::render(cfg.clone(), &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) @@ -144,7 +150,7 @@ async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Resul }) } -pub async fn load(dir: &str) -> Result<Vec<Post>> { +pub async fn load(cfg: Arc<Config>, dir: &str) -> Result<Vec<Post>> { let cli = match std::env::var("MI_TOKEN") { Ok(token) => mi::Client::new(token.to_string(), crate::APPLICATION_NAME.to_string()).ok(), Err(_) => None, @@ -152,7 +158,7 @@ pub async fn load(dir: &str) -> Result<Vec<Post>> { let futs = glob(&format!("{}/*.markdown", dir))? .filter_map(Result::ok) - .map(|fname| read_post(dir, fname, cli.borrow())); + .map(|fname| read_post(cfg.clone(), dir, fname, cli.borrow())); let mut result: Vec<Post> = futures::future::join_all(futs) .await @@ -172,25 +178,30 @@ pub async fn load(dir: &str) -> Result<Vec<Post>> { #[cfg(test)] mod tests { use super::*; + use crate::app::Config; use color_eyre::eyre::Result; + use std::sync::Arc; #[tokio::test] async fn blog() { let _ = pretty_env_logger::try_init(); - load("blog").await.expect("posts to load"); + let cfg = Arc::new(Config::default()); + load(cfg, "blog").await.expect("posts to load"); } #[tokio::test] async fn gallery() -> Result<()> { let _ = pretty_env_logger::try_init(); - load("gallery").await?; + let cfg = Arc::new(Config::default()); + load(cfg, "gallery").await?; Ok(()) } #[tokio::test] async fn talks() -> Result<()> { let _ = pretty_env_logger::try_init(); - load("talks").await?; + let cfg = Arc::new(Config::default()); + load(cfg, "talks").await?; Ok(()) } } diff --git a/src/signalboost.rs b/src/signalboost.rs index a57a976..3d1b534 100644 --- a/src/signalboost.rs +++ b/src/signalboost.rs @@ -16,7 +16,8 @@ mod tests { use color_eyre::eyre::Result; #[test] fn load() -> Result<()> { - let _people: Vec<super::Person> = serde_dhall::from_file("./signalboost.dhall").parse()?; + let _people: Vec<super::Person> = + serde_dhall::from_file("./dhall/signalboost.dhall").parse()?; Ok(()) } diff --git a/src/tmpl/mod.rs b/src/tmpl/mod.rs index 29d75f6..5391e9f 100644 --- a/src/tmpl/mod.rs +++ b/src/tmpl/mod.rs @@ -1,7 +1,27 @@ +use crate::app::Config; use maud::{html, Markup}; +use std::sync::Arc; pub mod nag; +pub fn xeblog_salary_history(cfg: Arc<Config>) -> Markup { + html! { + table.salary_history { + tr { + th { "Title" } + th { "Start Date" } + th { "End Date" } + th { "Days Worked" } + th { "Salary" } + th { "How I Left" } + } + @for job in &cfg.clone().job_history { + (job.pay_history_row()) + } + } + } +} + pub fn xeblog_hero(file: String, prompt: Option<String>) -> Markup { html! { figure.hero style="margin:0" { |
