aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorXe Iaso <me@christine.website>2022-06-14 15:04:17 -0400
committerGitHub <noreply@github.com>2022-06-14 15:04:17 -0400
commitad6fba4c79e8b5ab08e2f0db8bc4087f03151f7f (patch)
tree9888fe24eb3ea35ea0f9b54af8723b4a000e6ad9 /src
parent7541df778165b5a96da714256d011685b476abc0 (diff)
downloadxesite-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.rs9
-rw-r--r--src/app/mod.rs77
-rw-r--r--src/handlers/mod.rs28
-rw-r--r--src/main.rs6
-rw-r--r--src/post/mod.rs27
-rw-r--r--src/signalboost.rs3
-rw-r--r--src/tmpl/mod.rs20
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" {