From b5643889dd2066b3c99135ac0449e15541e40b36 Mon Sep 17 00:00:00 2001 From: Hampton Moore Date: Tue, 16 Jan 2024 15:02:04 -0500 Subject: [PATCH] Add back theming --- Cargo.lock | 34 +++++++++++++++++++++++++ Cargo.toml | 1 + assets/css/main.css | 23 +++++++++++++++++ src/main.rs | 62 +++++++++++++++++++++++++++++++++++---------- src/site/home.rs | 41 ++++++++++++++++++++++-------- src/site/mod.rs | 13 +++++----- src/site/things.rs | 22 +++++++++++----- src/site/words.rs | 35 ++++++++++++++++--------- src/things.rs | 5 ++-- src/update.rs | 11 +++++--- src/utils.rs | 9 +++---- src/words.rs | 46 +++++++++++++++++++++------------ 12 files changed, 226 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cadcdfd..c16bab2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-macros" version = "0.4.1" @@ -378,6 +400,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2779,6 +2812,7 @@ name = "www" version = "0.1.0" dependencies = [ "axum", + "axum-extra", "chrono", "color-eyre", "comrak", diff --git a/Cargo.toml b/Cargo.toml index dbad5f0..55730b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] axum = { version = "0.7.4", features = ["macros"] } +axum-extra = { version = "0.9.2", features = ["cookie"] } chrono = "0.4.31" color-eyre = "0.6.2" comrak = "0.20.0" diff --git a/assets/css/main.css b/assets/css/main.css index dedca6e..eb90bc3 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -183,4 +183,27 @@ body[colorscheme="white"] { --container-header-color: #191919; --container-backdrop: #000000; --text-color: #191919 +} + +body[colorscheme="white"] .themecolors div[theme="white"] { + border: 1px solid #191919; +} + +.themecolors { + bottom: 8px; + left: 8px; + display: block; + margin: auto; + position: absolute; +} + +.themecolors div { + width: 16px; + height: 16px; + float: left; + margin-left: 8px; +} + +.themecolors div:hover { + cursor: pointer; } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index bff0cdf..3ce9961 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,34 @@ +use axum::{response::Html, routing::get, Router}; +use log::{error, info}; use std::sync::Arc; use tokio::sync::RwLock; -use log::{info, error}; -use axum::{ - response::Html, - routing::{get}, - Router -}; use tower_http::services::ServeDir; mod site; +mod things; mod update; mod utils; -mod things; mod words; async fn health() -> Html { Html(String::from("OK")) } - -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ClientState { - pub theme : String, + pub theme: String, } +// use axum::extract::FromRef; + +// impl FromRef>> for ClientState { +// fn from_ref(_: &Arc>) -> Self { +// // Your logic to extract or construct ClientState from SiteState +// // For example: +// ClientState { +// theme: "default_theme".to_string(), +// } +// } +// } #[derive(Clone)] pub struct SiteState { @@ -36,7 +42,8 @@ async fn main() { env_logger::init(); info!("Starting up!"); - let things = things::read_things_from_file("./content/things.csv").expect("Failed to read things"); + let things = + things::read_things_from_file("./content/things.csv").expect("Failed to read things"); let words = words::init("./content/words/"); @@ -64,8 +71,37 @@ async fn main() { .route("/posts/:slug", get(site::words::post)) .route("/things/", get(site::things::index)) .route("/", get(site::home::home)) - .with_state(state); + .with_state(state) + .layer(middleware::from_fn(middleware_apply_client_state)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); -} \ No newline at end of file +} + +use axum::{ + extract::Request, + middleware::{self, Next}, + response::Response, +}; + +use axum_extra::extract::cookie::{CookieJar}; + +async fn middleware_apply_client_state( + jar: CookieJar, + mut request: Request, + next: Next, +) -> Response { + let mut state = ClientState { + theme: String::from("DEFAULT"), + }; + + if let Some(cookie) = jar.get("colorscheme") { + state.theme = cookie.value().to_string(); + } + + request.extensions_mut().insert(state); + + let response = next.run(request).await; + + response +} diff --git a/src/site/home.rs b/src/site/home.rs index 7ecb3a7..71bdc82 100644 --- a/src/site/home.rs +++ b/src/site/home.rs @@ -1,12 +1,15 @@ -use maud::{html, Markup}; +use super::base; +use crate::words::Post; +use crate::{ClientState, SiteState}; +use axum::extract::{Extension, State}; +use maud::{html, Markup, PreEscaped}; use std::sync::Arc; use tokio::sync::RwLock; -use axum::extract::State; -use crate::SiteState; -use crate::words::Post; -use super::base; -pub async fn home(State(state): State>>) -> Markup { +pub async fn home( + State(state): State>>, + Extension(client_state): Extension, +) -> Markup { let state = state.read().await; let things = state.things[0..5].to_vec(); @@ -21,10 +24,10 @@ pub async fn home(State(state): State>>) -> Markup { } let content = html! { - div class="pure-g hero section" { + div class="pure-g hero section" style="position: relative" { div class="pure-u-1 pure-u-md-2-3 hero-text" { h1 { "Hampton Moore" } - p { + p { "I am an network automations engineer, software developer, and student (CS BS @ UMass Amherst). Since Summer 2022 I have been working at " a href="https://arista.com" target = "_blank" { "Arista Networks" } @@ -37,6 +40,22 @@ pub async fn home(State(state): State>>) -> Markup { div class="pure-u-1 pure-u-md-1-3 hero-img" { img class="pure-img" src="/assets/img/hammy.png" alt="Hampton's avatar"; } + div class="themecolors" { + div style="background-color: #ff5757" theme="red" {} + div style="background-color: #9b5efd" theme="purple" {} + div style="background-color: #fafafa; box-sizing: border-box;" theme="white" {} + div style="background-color: #ffa7d1" theme="pink" {} + div style="background-color: #04a7e7" theme="blue" {} + } + script type="text/javascript" { + (PreEscaped(" + [...document.getElementsByClassName('themecolors')[0].children].forEach((c)=>c.onclick=()=>{ + theme = c.getAttribute('theme'); + document.body.setAttribute('colorscheme', theme); + document.cookie = `colorscheme=${theme}; expires=${(new Date(Date.now()+ 86400*365*1000)).toUTCString()}; path=/`; + }) + ")) + } } div class="pure-g hero section" { @@ -44,7 +63,7 @@ pub async fn home(State(state): State>>) -> Markup { h3 { "Things I've Made" } ul { @for thing in things.clone() { - li { + li { (thing.date.format("%Y-%m").to_string()) ": " a href=(thing.link) { (thing.title) } @if let Some(description) = &thing.description { @@ -62,7 +81,7 @@ pub async fn home(State(state): State>>) -> Markup { h3 { "Words I've Written" } ul { @for post in words { - li { + li { (post.date.format("%Y").to_string()) " " a href=(post.link) { (post.title) } ": " @@ -81,5 +100,5 @@ pub async fn home(State(state): State>>) -> Markup { }; - base("Home".to_owned(), content, state.clone()) + base("Home".to_owned(), content, state.clone(), client_state) } diff --git a/src/site/mod.rs b/src/site/mod.rs index 22ee76d..4a15e65 100644 --- a/src/site/mod.rs +++ b/src/site/mod.rs @@ -1,12 +1,10 @@ +use crate::{ClientState, SiteState}; use maud::{html, Markup}; -use crate::SiteState; -use axum::response::Html; pub mod home; -pub mod words; pub mod things; +pub mod words; -pub fn base(title: String, content: Markup, state: SiteState) -> Markup { - +pub fn base(title: String, content: Markup, _state: SiteState, client: ClientState) -> Markup { let description = "Hampton Moore"; let title = format!("{} | Hampton Moore", title); @@ -34,7 +32,7 @@ pub fn base(title: String, content: Markup, state: SiteState) -> Markup { meta property="og:theme-color" content="#19191e"; } - body { + body colorscheme=(client.theme) { div class="main" { (content); div class="footer" { @@ -57,7 +55,7 @@ pub fn base(title: String, content: Markup, state: SiteState) -> Markup { a target="_blank" href="https://umass.edu" { img src="/assets/img/badges/umass.gif" alt="umass"; } - a target="_blank" href="https://ezrizhu.com/" { + a target="_blank" href="https://ezrizhu.com/" { img src="/assets/img/badges/ezri.png" alt="Ezri"; } img src="/assets/img/badges/aperture_labs.jpg" alt="aperture_labs"; @@ -85,6 +83,7 @@ pub fn base(title: String, content: Markup, state: SiteState) -> Markup { a target="_blank" href="https://github.com/hamptonmoore/www/blob/main/COPYING" { "GNU AGPLv3 license" } br; "All opinions here are my own and do not reflect the views of my employers or university: future, past, and present." + br; } } } diff --git a/src/site/things.rs b/src/site/things.rs index db2d983..720f1c4 100644 --- a/src/site/things.rs +++ b/src/site/things.rs @@ -1,14 +1,17 @@ +use super::base; +use crate::{ClientState, SiteState}; +use axum::extract::{Extension, State}; use maud::{html, Markup}; use std::sync::Arc; use tokio::sync::RwLock; -use axum::extract::State; -use crate::SiteState; -use super::base; -pub async fn index(State(state): State>>) -> Markup { +pub async fn index( + State(state): State>>, + Extension(client_state): Extension, +) -> Markup { let state = state.read().await; - let things = state.things.clone(); + let things = state.things.clone(); let content = html! { div class="pure-g hero section" { @@ -16,7 +19,7 @@ pub async fn index(State(state): State>>) -> Markup { h1 { "Things I've Made" } ul { @for thing in things.clone() { - li { + li { (thing.date.format("%Y-%m-%d").to_string()) ": " a href=(thing.link) { (thing.title) } @if let Some(description) = &thing.description { @@ -34,5 +37,10 @@ pub async fn index(State(state): State>>) -> Markup { } }; - base("Things".to_owned(), content, state.clone()) + base( + "Things".to_owned(), + content, + state.clone(), + client_state, + ) } diff --git a/src/site/words.rs b/src/site/words.rs index c9548fe..f8996d0 100644 --- a/src/site/words.rs +++ b/src/site/words.rs @@ -1,17 +1,19 @@ +use super::base; +use crate::{ClientState, SiteState}; +use axum::extract::{Extension, Path, State}; use maud::{html, Markup, PreEscaped}; use std::sync::Arc; use tokio::sync::RwLock; -use axum::extract::{State, Path}; -use crate::SiteState; -use super::base; - -use crate::words::{get, Post}; +use crate::words::{get}; -pub async fn index(State(state): State>>) -> Markup { +pub async fn index( + State(state): State>>, + Extension(client_state): Extension, +) -> Markup { let state = state.read().await; - let words = state.words.clone(); + let words = state.words.clone(); let content = html! { div class="pure-g hero section" { @@ -19,7 +21,7 @@ pub async fn index(State(state): State>>) -> Markup { h1 { "Words I've Written" } ul { @for post in words.clone() { - li { + li { (post.date.format("%Y-%m-%d").to_string()) ": " a href=(post.link) { (post.title) } (" - ") @@ -35,10 +37,19 @@ pub async fn index(State(state): State>>) -> Markup { } }; - base("Posts".to_owned(), content, state.clone()) + base( + "Posts".to_owned(), + content, + state.clone(), + client_state, + ) } -pub async fn post(State(state): State>>, Path(slug): Path) -> Markup { +pub async fn post( + State(state): State>>, + Path(slug): Path, + Extension(client_state): Extension, +) -> Markup { let state = state.read().await; let words = state.words.clone(); @@ -61,5 +72,5 @@ pub async fn post(State(state): State>>, Path(slug): Path< } }; - base(post.title, content, state.clone()) -} \ No newline at end of file + base(post.title, content, state.clone(), client_state) +} diff --git a/src/things.rs b/src/things.rs index d3ba4e1..c66d562 100644 --- a/src/things.rs +++ b/src/things.rs @@ -18,7 +18,8 @@ pub fn read_things_from_file(file_path: &str) -> io::Result> { let mut things = Vec::new(); - for line in reader.lines().skip(1) { // Skip the header line + for line in reader.lines().skip(1) { + // Skip the header line let line = line?; let parts: Vec<&str> = line.split(',').collect(); @@ -47,4 +48,4 @@ pub fn read_things_from_file(file_path: &str) -> io::Result> { things.sort_by(|a, b| b.date.cmp(&a.date)); Ok(things) -} \ No newline at end of file +} diff --git a/src/update.rs b/src/update.rs index bab1c09..a3c7692 100644 --- a/src/update.rs +++ b/src/update.rs @@ -1,12 +1,17 @@ -use std::sync::Arc; -use tokio::sync::RwLock; use crate::SiteState; use color_eyre::eyre::Result; +use std::sync::Arc; +use tokio::sync::RwLock; pub async fn update(state: Arc>) -> Result<()> { let mut state = state.write().await; - let last_updated_text = String::from(chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string().as_str()); + let last_updated_text = String::from( + chrono::Local::now() + .format("%Y-%m-%d %H:%M:%S") + .to_string() + .as_str(), + ); state.last_updated = last_updated_text; Ok(()) diff --git a/src/utils.rs b/src/utils.rs index de99efb..48e97b7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,7 @@ use comrak::{markdown_to_html, ComrakOptions}; use kuchiki::traits::*; -use std::fs::{self,File}; -use std::io; -use std::io::prelude::*; + + pub fn md_to_html(md: &str) -> String { @@ -20,13 +19,13 @@ pub fn add_target_blank_to_links(html: String) -> String { // Get all `a` elements in the document let a_elements = document.select("a").unwrap(); - + for a in a_elements { let mut attrs = a.attributes.borrow_mut(); // Add the target "_blank" attribute attrs.insert("target", "_blank".to_string()); } - + // Serialize the modified document back to a string let body = document.select_first("body").unwrap(); let mut output = vec![]; diff --git a/src/words.rs b/src/words.rs index 8291711..3b42fbb 100644 --- a/src/words.rs +++ b/src/words.rs @@ -1,8 +1,8 @@ -use std::fs; use super::utils; -use gray_matter::Matter; -use gray_matter::engine::YAML; use chrono::prelude::*; +use gray_matter::engine::YAML; +use gray_matter::Matter; +use std::fs; #[derive(Clone)] pub enum PostType { @@ -15,14 +15,13 @@ pub struct Post { pub slug: String, pub link: String, pub title: String, - pub date: DateTime, + pub date: NaiveDate, pub description: String, pub tags: Vec, pub r#type: PostType, pub body: String, } - pub fn get(posts: Vec, slug: &str) -> Option { for post in posts { if post.slug == slug { @@ -48,10 +47,12 @@ pub fn init(dir: &str) -> Vec { let matter = Matter::::new(); let result = matter.parse(&raw); - let Some(result_map) = result.data.as_ref() - else { panic!("Error parsing YAML") }; - let Ok(result_map) = result_map.as_hashmap() - else { panic!("Error getting hashmap from Pod") }; + let Some(result_map) = result.data.as_ref() else { + panic!("Error parsing YAML") + }; + let Ok(result_map) = result_map.as_hashmap() else { + panic!("Error getting hashmap from Pod") + }; let title = result_map["title"].as_string().unwrap(); let description = result_map["description"].as_string().unwrap(); @@ -69,17 +70,21 @@ pub fn init(dir: &str) -> Vec { // The date_str will be displayed on the homepage, blogindex, and blog pages. // First we parse our text into NaiveDate let date = NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").unwrap(); - // Now we add time to it - let date = NaiveDateTime::new(date, NaiveTime::from_hms_opt(0, 0, 0).unwrap()); - // Now we make this a Fixed DateTime with Eastern time - let timezone_east = FixedOffset::east_opt(8 * 60 * 60).unwrap(); - let date = DateTime::::from_local(date, timezone_east); // If there is a link we don't load body text let link = result_map.get("link").map(|s| s.as_string().unwrap()); if let Some(link) = link { - let post = Post { slug: link.clone(), link, title, date, description, tags, r#type: PostType::Link, body: "".to_string() }; + let post = Post { + slug: link.clone(), + link, + title, + date, + description, + tags, + r#type: PostType::Link, + body: "".to_string(), + }; posts_list.push(post); continue; } @@ -87,7 +92,16 @@ pub fn init(dir: &str) -> Vec { // the markdown without the frontmatter, parsed to html let body = utils::md_to_html(&result.content); let slug = filename.replace(".md", ""); - let post = Post { link: format!("/posts/{}", slug), slug, title, date, description, tags, body, r#type: PostType::Post }; + let post = Post { + link: format!("/posts/{}", slug), + slug, + title, + date, + description, + tags, + body, + r#type: PostType::Post, + }; posts_list.push(post); } }