Merge pull request #21 from michalvankodev/misc/migrate-tailwind
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				test / cargo test (push) Failing after 1m46s
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	test / cargo test (push) Failing after 1m46s
				
			Tailwind migration
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -23,3 +23,6 @@ Cargo.lock | ||||
|  | ||||
| # Image generator | ||||
| generated_images/ | ||||
|  | ||||
| node_modules/ | ||||
| package-lock.json | ||||
|   | ||||
| @@ -1,29 +1,26 @@ | ||||
| layout { | ||||
|     pane split_direction="vertical" focus=true { | ||||
|         pane edit="src/main.rs" | ||||
|         pane split_direction="horizontal" size=60 { | ||||
|             just { args "server_dev"; } | ||||
|             just { args "test"; } | ||||
|     default_tab_template { | ||||
|         children | ||||
|         pane size=2 borderless=true { | ||||
|             plugin location="zellij:status-bar" | ||||
|         } | ||||
|         pane size=1 borderless=true { | ||||
|             plugin location="zellij:tab-bar" | ||||
|         } | ||||
|     } | ||||
|     pane_template name="just" { | ||||
|         command "just" | ||||
|         start_suspended true | ||||
|     } | ||||
| 	floating_panes { | ||||
| 		pane { | ||||
|             command "just" | ||||
|             args "tailwind" | ||||
|  | ||||
|     tab split_direction="vertical" focus=true { | ||||
|         pane  { | ||||
|                 just { args "server_dev"; } | ||||
|                 just { args "test"; } | ||||
|         } | ||||
| 		pane { | ||||
|             command "just" | ||||
|             args "decap_server" | ||||
|     	pane { | ||||
|              just { args "tailwind"; } | ||||
|              just { args "decap_server"; } | ||||
|         } | ||||
| 	} | ||||
|     pane size=2 borderless=true { | ||||
|         plugin location="zellij:status-bar" | ||||
|     } | ||||
|     pane size=1 borderless=true { | ||||
|         plugin location="zellij:tab-bar" | ||||
|     } | ||||
|     tab | ||||
| } | ||||
|   | ||||
							
								
								
									
										24
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -6,8 +6,7 @@ edition = "2021" | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
|  | ||||
| [dependencies] | ||||
| askama = { version = "0.14", features = ["with-axum", "mime", "mime_guess"] } | ||||
| askama_axum = "0.5.0" | ||||
| askama = { version = "0.14" } | ||||
| axum = "0.8.0" | ||||
| chrono = { version = "0.4.31", features = ["serde"] } | ||||
| pulldown-cmark = { version = "0.13" } | ||||
| @@ -28,22 +27,5 @@ indoc = "2.0.5" | ||||
| askama_escape = "0.13.0" | ||||
| mime_guess = "2.0.5" | ||||
|  | ||||
| [build] | ||||
| rustflags = ["-Z", "threads=8"] | ||||
|  | ||||
| # [target.x86_64-unknown-linux-gnu] | ||||
| # rustflags = [ | ||||
| #     "-C", "link-arg=-fuse-ld=lld" | ||||
| # ] | ||||
|  | ||||
| [profile.dev] | ||||
| debug = true | ||||
| opt-level = 0 | ||||
| # codegen-units = 16 | ||||
| # lto = "thin" | ||||
| panic = "unwind" | ||||
| strip = false | ||||
| incremental = true | ||||
|  | ||||
| [profile.dev.package.askama_derive] | ||||
| opt-level = 3 | ||||
| [dev-dependencies] | ||||
| pretty_assertions = "1" | ||||
|   | ||||
							
								
								
									
										2
									
								
								justfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								justfile
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ port := env_var_or_default('PORT', '3080') | ||||
|  | ||||
| # Tailwind in watch mode | ||||
| tailwind: | ||||
| 	npx tailwindcss -i ./styles/input.css -o ./styles/output.css --watch | ||||
| 	npx @tailwindcss/cli -i ./styles/input.css -o ./styles/output.css --watch | ||||
|  | ||||
| # svg sprite creation | ||||
| svgstore: | ||||
|   | ||||
							
								
								
									
										6
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "dependencies": { | ||||
|     "@tailwindcss/cli": "^4.1.10", | ||||
|     "tailwindcss": "^4.1.10" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/feed.rs
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/feed.rs
									
									
									
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| use askama::Values; | ||||
| use axum::http::{header, StatusCode}; | ||||
| use axum::response::IntoResponse; | ||||
| use chrono::Utc; | ||||
| @@ -7,6 +8,14 @@ use crate::blog_posts::blog_post_model::{BlogPostMetadata, BLOG_POST_PATH}; | ||||
| use crate::filters::{parse_markdown, truncate_md}; | ||||
| use crate::post_utils::post_listing::get_post_list; | ||||
|  | ||||
| struct EmptyValues; | ||||
|  | ||||
| impl Values for EmptyValues { | ||||
|     fn get_value<'a>(&'a self, _key: &str) -> Option<&'a dyn std::any::Any> { | ||||
|         return None; | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> { | ||||
|     let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH) | ||||
|         .await | ||||
| @@ -27,14 +36,14 @@ pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> { | ||||
|                 .title(Some(post.metadata.title)) | ||||
|                 .link(Some(format!("https://michalvanko.dev/blog/{}", post.slug))) | ||||
|                 .description({ | ||||
|                     let truncated = | ||||
|                         truncate_md(&post.body, 2).unwrap_or("Can't parse post body".to_string()); | ||||
|                     let parsed_md = parse_markdown(&truncated) | ||||
|                     let truncated = truncate_md(&post.body, &EmptyValues, 2) | ||||
|                         .unwrap_or("Can't parse post body".to_string()); | ||||
|                     let parsed_md = parse_markdown(&truncated, &EmptyValues) | ||||
|                         .unwrap_or("Can't process truncated post body".to_string()); | ||||
|                     Some(parsed_md) | ||||
|                 }) | ||||
|                 .content({ | ||||
|                     let parsed_md = parse_markdown(&post.body) | ||||
|                     let parsed_md = parse_markdown(&post.body, &EmptyValues) | ||||
|                         .unwrap_or("Can't process full post body".to_string()); | ||||
|                     Some(parsed_md) | ||||
|                 }) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| use core::fmt; | ||||
| use std::path::Path; | ||||
|  | ||||
| use image::image_dimensions; | ||||
| @@ -19,7 +20,10 @@ enum TextKind { | ||||
| } | ||||
|  | ||||
| // pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> | ||||
| pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> { | ||||
| pub fn parse_markdown<T: fmt::Display>( | ||||
|     markdown: T, | ||||
|     _: &dyn askama::Values, | ||||
| ) -> ::askama::Result<String> { | ||||
|     let mut options = Options::empty(); | ||||
|     options.insert(Options::ENABLE_TABLES); | ||||
|     options.insert(Options::ENABLE_FOOTNOTES); | ||||
| @@ -34,7 +38,8 @@ pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> { | ||||
|     let theme = theme_set.themes.get("InspiredGitHub").unwrap(); | ||||
|     let mut heading_ended: Option<bool> = None; | ||||
|  | ||||
|     let parser = Parser::new_ext(markdown, options).map(|event| match event { | ||||
|     let mds = markdown.to_string(); | ||||
|     let parser = Parser::new_ext(&mds, options).map(|event| match event { | ||||
|         /* | ||||
|         Parsing images considers `alt` attribute as inner `Text` event | ||||
|         Therefore the `[alt]` is rendered in html as subtitle | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| use chrono::{DateTime, Utc}; | ||||
|  | ||||
| // This filter does not have extra arguments | ||||
| pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> { | ||||
| pub fn pretty_date(date_time: &DateTime<Utc>, _: &dyn askama::Values) -> ::askama::Result<String> { | ||||
|     let formatted = format!("{}", date_time.format("%e %B %Y")); | ||||
|     Ok(formatted) | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"]; | ||||
|  | ||||
| pub fn truncate_md(body: &str, rows: usize) -> ::askama::Result<String> { | ||||
| pub fn truncate_md(body: &str, _: &dyn askama::Values, rows: usize) -> ::askama::Result<String> { | ||||
|     let description = body | ||||
|         .lines() | ||||
|         .filter(|line| { | ||||
|   | ||||
| @@ -1,9 +1,17 @@ | ||||
| use askama::Template; | ||||
| use axum::{ | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "admin.html")] | ||||
| pub struct AdminPageTemplate {} | ||||
|  | ||||
| pub async fn render_admin() -> AdminPageTemplate { | ||||
|     AdminPageTemplate {} | ||||
| pub async fn render_admin() -> Result<impl IntoResponse, StatusCode> { | ||||
|     Ok(Html( | ||||
|         AdminPageTemplate {} | ||||
|             .render() | ||||
|             .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| use askama::Template; | ||||
| use axum::http::StatusCode; | ||||
| use axum::{ | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "assets/animated_logo.html")] | ||||
| pub struct AnimatedLogoTemplate {} | ||||
|  | ||||
| pub async fn render_animated_logo() -> Result<AnimatedLogoTemplate, StatusCode> { | ||||
|     Ok(AnimatedLogoTemplate {}) | ||||
| pub async fn render_animated_logo() -> Result<impl IntoResponse, StatusCode> { | ||||
|     Ok(Html(AnimatedLogoTemplate {}.render().unwrap())) | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| use askama::Template; | ||||
| use axum::{ | ||||
|     extract::{OriginalUri, Path}, | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
| use tokio::try_join; | ||||
| use tracing::debug; | ||||
| @@ -22,7 +24,7 @@ use super::post_list::PostListTemplate; | ||||
| pub async fn render_blog_post_list( | ||||
|     tag: Option<Path<String>>, | ||||
|     OriginalUri(original_uri): OriginalUri, | ||||
| ) -> Result<PostListTemplate, StatusCode> { | ||||
| ) -> Result<impl IntoResponse, StatusCode> { | ||||
|     // I will forget what happens here in a week. But essentially it's pattern matching and shadowing | ||||
|     let tag = tag.map(|Path(tag)| tag); | ||||
|  | ||||
| @@ -50,14 +52,18 @@ pub async fn render_blog_post_list( | ||||
|         ("Blog posts".to_string(), "Blog posts".to_string()) | ||||
|     }; | ||||
|  | ||||
|     Ok(PostListTemplate { | ||||
|         title, | ||||
|         og_title, | ||||
|         segment: Segment::Blog, | ||||
|         posts, | ||||
|         header_props, | ||||
|         tags: blog_tags, | ||||
|         featured_projects, | ||||
|         current_url: original_uri.to_string(), | ||||
|     }) | ||||
|     Ok(Html( | ||||
|         PostListTemplate { | ||||
|             title, | ||||
|             og_title, | ||||
|             segment: Segment::Blog, | ||||
|             posts, | ||||
|             header_props, | ||||
|             tags: blog_tags, | ||||
|             featured_projects, | ||||
|             current_url: original_uri.to_string(), | ||||
|         } | ||||
|         .render() | ||||
|         .unwrap(), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| use askama::Template; | ||||
| use axum::extract::OriginalUri; | ||||
| use axum::response::{Html, IntoResponse}; | ||||
| use axum::{extract::Path, http::StatusCode}; | ||||
| use chrono::{DateTime, Utc}; | ||||
|  | ||||
| @@ -30,7 +31,7 @@ pub struct BlogPostTemplate { | ||||
| pub async fn render_blog_post( | ||||
|     Path(post_id): Path<String>, | ||||
|     OriginalUri(original_uri): OriginalUri, | ||||
| ) -> Result<BlogPostTemplate, StatusCode> { | ||||
| ) -> Result<impl IntoResponse, StatusCode> { | ||||
|     let path = format!("{}/{}.md", BLOG_POST_PATH, post_id); | ||||
|     let post = parse_post::<BlogPostMetadata>(&path).await?; | ||||
|     let segment = if original_uri.to_string().starts_with("/blog") { | ||||
| @@ -59,17 +60,21 @@ pub async fn render_blog_post( | ||||
|         _ => HeaderProps::default(), | ||||
|     }; | ||||
|  | ||||
|     Ok(BlogPostTemplate { | ||||
|         title: post.metadata.title, | ||||
|         date: post.metadata.date, | ||||
|         tags: post.metadata.tags, | ||||
|         body: post.body, | ||||
|         slug: post.slug, | ||||
|         segment, | ||||
|         thumbnail: post.metadata.thumbnail, | ||||
|         header_props, | ||||
|         recommended_posts, | ||||
|     }) | ||||
|     Ok(Html( | ||||
|         BlogPostTemplate { | ||||
|             title: post.metadata.title, | ||||
|             date: post.metadata.date, | ||||
|             tags: post.metadata.tags, | ||||
|             body: post.body, | ||||
|             slug: post.slug, | ||||
|             segment, | ||||
|             thumbnail: post.metadata.thumbnail, | ||||
|             header_props, | ||||
|             recommended_posts, | ||||
|         } | ||||
|         .render() | ||||
|         .unwrap(), | ||||
|     )) | ||||
| } | ||||
|  | ||||
| async fn get_recommended_posts( | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| use askama::Template; | ||||
| use axum::{ | ||||
|     extract::{OriginalUri, Path}, | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
| use tokio::try_join; | ||||
| use tracing::debug; | ||||
| @@ -21,7 +23,7 @@ use super::post_list::PostListTemplate; | ||||
| pub async fn render_broadcast_post_list( | ||||
|     tag: Option<Path<String>>, | ||||
|     OriginalUri(original_uri): OriginalUri, | ||||
| ) -> Result<PostListTemplate, StatusCode> { | ||||
| ) -> Result<impl IntoResponse, StatusCode> { | ||||
|     // I will forget what happens here in a week. But essentially it's pattern matching and shadowing | ||||
|     let tag = tag.map(|Path(tag)| tag); | ||||
|  | ||||
| @@ -50,14 +52,18 @@ pub async fn render_broadcast_post_list( | ||||
|         "Broadcasts".to_string() | ||||
|     }; | ||||
|  | ||||
|     Ok(PostListTemplate { | ||||
|         title: title.clone(), | ||||
|         og_title: title, | ||||
|         segment: Segment::Broadcasts, | ||||
|         posts, | ||||
|         header_props, | ||||
|         tags: popular_tags, | ||||
|         featured_projects, | ||||
|         current_url: original_uri.to_string(), | ||||
|     }) | ||||
|     Ok(Html( | ||||
|         PostListTemplate { | ||||
|             title: title.clone(), | ||||
|             og_title: title, | ||||
|             segment: Segment::Broadcasts, | ||||
|             posts, | ||||
|             header_props, | ||||
|             tags: popular_tags, | ||||
|             featured_projects, | ||||
|             current_url: original_uri.to_string(), | ||||
|         } | ||||
|         .render() | ||||
|         .unwrap(), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| use askama::Template; | ||||
| use axum::http::StatusCode; | ||||
| use axum::{ | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
|  | ||||
| use crate::components::site_header::HeaderProps; | ||||
|  | ||||
| @@ -18,7 +21,7 @@ pub struct ContactPageTemplate { | ||||
|     pub links: Vec<ContactLink>, | ||||
| } | ||||
|  | ||||
| pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> { | ||||
| pub async fn render_contact() -> Result<impl IntoResponse, StatusCode> { | ||||
|     let links = vec![ | ||||
|         ContactLink { | ||||
|             href: "mailto:michalvankosk@gmail.com".to_string(), | ||||
| @@ -76,9 +79,13 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> { | ||||
|         }, | ||||
|     ]; | ||||
|  | ||||
|     Ok(ContactPageTemplate { | ||||
|         title: "Contact".to_owned(), | ||||
|         header_props: HeaderProps::default(), | ||||
|         links, | ||||
|     }) | ||||
|     Ok(Html( | ||||
|         ContactPageTemplate { | ||||
|             title: "Contact".to_owned(), | ||||
|             header_props: HeaderProps::default(), | ||||
|             links, | ||||
|         } | ||||
|         .render() | ||||
|         .unwrap(), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| use std::rc::Rc; | ||||
|  | ||||
| use askama::Template; | ||||
| use axum::http::StatusCode; | ||||
| use axum::{ | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
| use tokio::try_join; | ||||
|  | ||||
| use crate::{ | ||||
| @@ -26,7 +29,7 @@ pub struct IndexTemplate { | ||||
|     featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>, | ||||
| } | ||||
|  | ||||
| pub async fn render_index() -> Result<IndexTemplate, StatusCode> { | ||||
| pub async fn render_index() -> Result<impl IntoResponse, StatusCode> { | ||||
|     let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!( | ||||
|         get_popular_tags(Some(Segment::Blog)), | ||||
|         get_popular_tags(Some(Segment::Broadcasts)), | ||||
| @@ -44,12 +47,16 @@ pub async fn render_index() -> Result<IndexTemplate, StatusCode> { | ||||
|     let featured_broadcasts = | ||||
|         ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]); | ||||
|  | ||||
|     Ok(IndexTemplate { | ||||
|         header_props: HeaderProps::default(), | ||||
|         blog_tags, | ||||
|         broadcasts_tags, | ||||
|         featured_blog_posts, | ||||
|         featured_projects, | ||||
|         featured_broadcasts, | ||||
|     }) | ||||
|     Ok(Html( | ||||
|         IndexTemplate { | ||||
|             header_props: HeaderProps::default(), | ||||
|             blog_tags, | ||||
|             broadcasts_tags, | ||||
|             featured_blog_posts, | ||||
|             featured_projects, | ||||
|             featured_broadcasts, | ||||
|         } | ||||
|         .render() | ||||
|         .unwrap(), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| use askama::Template; | ||||
| use axum::{extract::OriginalUri, http::StatusCode}; | ||||
| use axum::{ | ||||
|     extract::OriginalUri, | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
| use tracing::info; | ||||
|  | ||||
| use crate::components::site_header::HeaderProps; | ||||
| @@ -13,13 +17,17 @@ pub struct NotFoundPage { | ||||
|  | ||||
| pub async fn render_not_found( | ||||
|     OriginalUri(original_uri): OriginalUri, | ||||
| ) -> Result<(StatusCode, NotFoundPage), StatusCode> { | ||||
| ) -> Result<(StatusCode, impl IntoResponse), StatusCode> { | ||||
|     info!("{original_uri} not found"); | ||||
|     Ok(( | ||||
|         StatusCode::NOT_FOUND, | ||||
|         NotFoundPage { | ||||
|             title: "This page does not exists".to_owned(), | ||||
|             header_props: HeaderProps::default(), | ||||
|         }, | ||||
|         Html( | ||||
|             NotFoundPage { | ||||
|                 title: "This page does not exists".to_owned(), | ||||
|                 header_props: HeaderProps::default(), | ||||
|             } | ||||
|             .render() | ||||
|             .unwrap(), | ||||
|         ), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| use askama::Template; | ||||
| use axum::http::StatusCode; | ||||
| use axum::{ | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
| use serde::Deserialize; | ||||
|  | ||||
| use crate::{ | ||||
| @@ -50,7 +53,7 @@ pub struct PortfolioTemplate { | ||||
|     pub technology_list: Vec<String>, | ||||
| } | ||||
|  | ||||
| pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> { | ||||
| pub async fn render_portfolio() -> Result<impl IntoResponse, StatusCode> { | ||||
|     let portfolio = parse_post::<PortfolioPageModel>("_pages/portfolio.md").await?; | ||||
|  | ||||
|     let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?; | ||||
| @@ -126,14 +129,18 @@ pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> { | ||||
|     .map(|str| str.to_owned()) | ||||
|     .collect(); | ||||
|  | ||||
|     Ok(PortfolioTemplate { | ||||
|         title: "Portfolio".to_owned(), | ||||
|         body: portfolio.body, | ||||
|         header_props: HeaderProps::default(), | ||||
|         project_list, | ||||
|         workplace_list, | ||||
|         education_list, | ||||
|         contact_links, | ||||
|         technology_list, | ||||
|     }) | ||||
|     Ok(Html( | ||||
|         PortfolioTemplate { | ||||
|             title: "Portfolio".to_owned(), | ||||
|             body: portfolio.body, | ||||
|             header_props: HeaderProps::default(), | ||||
|             project_list, | ||||
|             workplace_list, | ||||
|             education_list, | ||||
|             contact_links, | ||||
|             technology_list, | ||||
|         } | ||||
|         .render() | ||||
|         .unwrap(), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| use askama::Template; | ||||
| use axum::http::StatusCode; | ||||
| use axum::{ | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
|  | ||||
| use crate::{ | ||||
|     components::site_header::HeaderProps, | ||||
| @@ -16,16 +19,20 @@ pub struct ProjectListTemplate { | ||||
|     pub header_props: HeaderProps, | ||||
| } | ||||
|  | ||||
| pub async fn render_projects_list() -> Result<ProjectListTemplate, StatusCode> { | ||||
| pub async fn render_projects_list() -> Result<impl IntoResponse, StatusCode> { | ||||
|     let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?; | ||||
|  | ||||
|     project_list.sort_by_key(|post| post.slug.to_string()); | ||||
|     project_list.retain(|project| project.metadata.displayed); | ||||
|     project_list.reverse(); | ||||
|  | ||||
|     Ok(ProjectListTemplate { | ||||
|         title: "Showcase".to_owned(), | ||||
|         header_props: HeaderProps::default(), | ||||
|         project_list, | ||||
|     }) | ||||
|     Ok(Html( | ||||
|         ProjectListTemplate { | ||||
|             title: "Showcase".to_owned(), | ||||
|             header_props: HeaderProps::default(), | ||||
|             project_list, | ||||
|         } | ||||
|         .render() | ||||
|         .unwrap(), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| use askama::Template; | ||||
| use axum::http::StatusCode; | ||||
| use axum::{ | ||||
|     http::StatusCode, | ||||
|     response::{Html, IntoResponse}, | ||||
| }; | ||||
|  | ||||
| use crate::components::site_header::HeaderProps; | ||||
|  | ||||
| @@ -9,8 +12,12 @@ pub struct EggFetcherShowcaseTemplate { | ||||
|     header_props: HeaderProps, | ||||
| } | ||||
|  | ||||
| pub async fn render_egg_fetcher() -> Result<EggFetcherShowcaseTemplate, StatusCode> { | ||||
|     Ok(EggFetcherShowcaseTemplate { | ||||
|         header_props: HeaderProps::default(), | ||||
|     }) | ||||
| pub async fn render_egg_fetcher() -> Result<impl IntoResponse, StatusCode> { | ||||
|     Ok(Html( | ||||
|         EggFetcherShowcaseTemplate { | ||||
|             header_props: HeaderProps::default(), | ||||
|         } | ||||
|         .render() | ||||
|         .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR), | ||||
|     )) | ||||
| } | ||||
|   | ||||
| @@ -262,7 +262,8 @@ pub fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> { | ||||
| fn test_get_export_formats() { | ||||
|     assert_eq!( | ||||
|         get_export_formats("/images/uploads/img_name.jpg"), | ||||
|         vec![ExportFormat::Avif, ExportFormat::Jpeg] | ||||
|         // vec![ExportFormat::Avif, ExportFormat::Jpeg] | ||||
|         vec![ExportFormat::Jpeg] | ||||
|     ) | ||||
| } | ||||
| #[test] | ||||
| @@ -292,24 +293,21 @@ fn test_generate_picture_markup() { | ||||
|     let height = 200; | ||||
|     let orig_img_path = "/images/uploads/2020-03-23_20-24-06_393.jpg"; | ||||
|     let result = indoc! { | ||||
|         r#"<picture> | ||||
|             <source | ||||
|                 srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.avif 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.avif 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.avif 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.avif 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.avif 4x" | ||||
|                 type="image/avif" | ||||
|             > | ||||
| <source | ||||
|                 srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.jpg 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.jpg 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.jpg 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.jpg 4x" | ||||
|                 type="image/jpeg" | ||||
|             > | ||||
|             <img | ||||
|             src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg" | ||||
|             width="300" | ||||
|             height="200" | ||||
|             alt="Testing image alt" | ||||
|         > | ||||
|         </picture>"#, | ||||
| r#"<picture> | ||||
|     <source | ||||
|     srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.jpg 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.jpg 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.jpg 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.jpg 4x" | ||||
|     type="image/jpeg" | ||||
| > | ||||
|     <img | ||||
|     src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg" | ||||
|     width="300" | ||||
|     height="200" | ||||
|     alt="Testing image alt" | ||||
|      | ||||
| > | ||||
| </picture>"#, | ||||
|     }; | ||||
|     assert_eq!( | ||||
|     pretty_assertions::assert_eq!( | ||||
|         generate_picture_markup(orig_img_path, width, height, "Testing image alt", None,) | ||||
|             .expect("picture markup has to be generated"), | ||||
|         result | ||||
|   | ||||
| @@ -8,7 +8,14 @@ use crate::{ | ||||
|         project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher, | ||||
|     }, | ||||
| }; | ||||
| use axum::{extract::MatchedPath, http::Request, routing::get, Router}; | ||||
| use askama::Template; | ||||
| use axum::{ | ||||
|     extract::MatchedPath, | ||||
|     http::{Request, StatusCode}, | ||||
|     response::IntoResponse, | ||||
|     routing::get, | ||||
|     Router, | ||||
| }; | ||||
| use tower_http::trace::TraceLayer; | ||||
| use tracing::info_span; | ||||
|  | ||||
| @@ -16,15 +23,15 @@ pub fn get_router() -> Router { | ||||
|     Router::new() | ||||
|         .route("/", get(render_index)) | ||||
|         .route("/blog", get(render_blog_post_list)) | ||||
|         .route("/blog/tags/:tag", get(render_blog_post_list)) | ||||
|         .route("/blog/:post_id", get(render_blog_post)) | ||||
|         .route("/blog/tags/{tag}", get(render_blog_post_list)) | ||||
|         .route("/blog/{post_id}", get(render_blog_post)) | ||||
|         .route("/broadcasts", get(render_broadcast_post_list)) | ||||
|         .route("/broadcasts/tags/:tag", get(render_broadcast_post_list)) | ||||
|         .route("/broadcasts/:post_id", get(render_blog_post)) | ||||
|         .route("/broadcasts/tags/{tag}", get(render_broadcast_post_list)) | ||||
|         .route("/broadcasts/{post_id}", get(render_blog_post)) | ||||
|         .route("/contact", get(render_contact)) | ||||
|         .route("/showcase", get(render_projects_list)) | ||||
|         .route("/showcase/m-logo-svg", get(render_animated_logo)) | ||||
|         .route("/showcase/:project_slug", get(render_egg_fetcher)) | ||||
|         .route("/showcase/{project_slug}", get(render_egg_fetcher)) | ||||
|         .route("/portfolio", get(render_portfolio)) | ||||
|         .route("/admin", get(render_admin)) | ||||
|         .route("/feed.xml", get(render_rss_feed)) | ||||
|   | ||||
							
								
								
									
										162
									
								
								styles/input.css
									
									
									
									
									
								
							
							
						
						
									
										162
									
								
								styles/input.css
									
									
									
									
									
								
							| @@ -1,63 +1,113 @@ | ||||
| @tailwind base; | ||||
| @tailwind components; | ||||
| @tailwind utilities; | ||||
| @import "tailwindcss"; | ||||
|  | ||||
| @theme { | ||||
|   --font-sans: "Baloo2", "Baloo2 Noto Fallback", "Baloo2 Fallback", "ui-sans-serif", "system-ui", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; | ||||
|  | ||||
|   --text-readxl: 1.75rem; | ||||
|   --text-readxl--line-height: 2.25rem; | ||||
|   --text-readxl--letter-spacing: -0.015rem; | ||||
|   --text-readxl--font-weight: 400; | ||||
|  | ||||
|   --spacing-note: 60rem; | ||||
|   --spacing-read: 64rem; | ||||
|   --spacing-image: min(70rem, 95vw); | ||||
|   --spacing-maxindex: 100rem; | ||||
|  | ||||
|   --color-blue-50: #f1f7fe; | ||||
|   --color-blue-100: #e1effd; | ||||
|   --color-blue-200: #bddefa; | ||||
|   --color-blue-300: #82c3f7; | ||||
|   --color-blue-400: #42a6f0; | ||||
|   --color-blue-500: #1789e0; | ||||
|   --color-blue-600: #0a6cbf; | ||||
|   --color-blue-700: #0a569a; | ||||
|   --color-blue-800: #0c4980; | ||||
|   --color-blue-900: #103e6a; | ||||
|   --color-blue-950: #0b2746; | ||||
|  | ||||
|   --color-pink-50: #fff4fd; | ||||
|   --color-pink-100: #ffe7fb; | ||||
|   --color-pink-200: #ffcff7; | ||||
|   --color-pink-300: #fea6eb; | ||||
|   --color-pink-400: #fc76dd; | ||||
|   --color-pink-500: #f342ca; | ||||
|   --color-pink-600: #d722a9; | ||||
|   --color-pink-700: #b31889; | ||||
|   --color-pink-800: #92166e; | ||||
|   --color-pink-900: #771859; | ||||
|   --color-pink-950: #500238; | ||||
|  | ||||
|   --color-purple-50: #F8F5FC; | ||||
|   --color-purple-100: #D5C2ED; | ||||
|   --color-purple-200: #B28EDE; | ||||
|   --color-purple-300: #8F5BCF; | ||||
|   --color-purple-400: #6D30B9; | ||||
|   --color-purple-500: #5F2AA2; | ||||
|   --color-purple-600: #52248A; | ||||
|   --color-purple-700: #441E73; | ||||
|   --color-purple-800: #36185C; | ||||
|   --color-purple-900: #281244; | ||||
|   --color-purple-950: #1A0C2D; | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: 'Baloo2'; | ||||
|   font-style: normal; | ||||
|   font-display: swap; | ||||
|   src: | ||||
|     local('Baloo2'), | ||||
|     url(/fonts/baloo2/Baloo2-Latin-Variable-wght.woff2) format('woff2'); | ||||
| } | ||||
|  | ||||
| /* latin-ext */ | ||||
| @font-face { | ||||
|   font-family: 'Baloo 2'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400 800; | ||||
|   font-display: swap; | ||||
|   src: url(/fonts/baloo2/Baloo2-Latin-Variable-ext-wght.woff2) format('woff2'); | ||||
|   unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, | ||||
|     U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: 'Baloo2 Fallback'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'), | ||||
|     local('Arial'); | ||||
|   ascent-override: 111.2%; | ||||
|   descent-override: 54.05%; | ||||
|   line-gap-override: 0%; | ||||
|   size-adjust: 96.95%; | ||||
| } | ||||
|  | ||||
| @font-face { | ||||
|   font-family: 'Baloo2 Noto Fallback'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local('Noto Sans'); | ||||
|   ascent-override: 88%; | ||||
|   descent-override: none; | ||||
|   line-gap-override: 0%; | ||||
|   size-adjust: 92%; | ||||
| } | ||||
|  | ||||
| @layer base { | ||||
|   @font-face { | ||||
|     font-family: 'Baloo2'; | ||||
|     font-style: normal; | ||||
|     font-display: swap; | ||||
|     src: | ||||
|       local('Baloo2'), | ||||
|       url(/fonts/baloo2/Baloo2-Latin-Variable-wght.woff2) format('woff2'); | ||||
|   } | ||||
|   /* latin-ext */ | ||||
|   @font-face { | ||||
|     font-family: 'Baloo 2'; | ||||
|     font-style: normal; | ||||
|     font-weight: 400 800; | ||||
|     font-display: swap; | ||||
|     src: url(/fonts/baloo2/Baloo2-Latin-Variable-ext-wght.woff2) format('woff2'); | ||||
|     unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, | ||||
|       U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | ||||
|   a { | ||||
|     @apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500; | ||||
|   } | ||||
|  | ||||
|   @font-face { | ||||
|     font-family: 'Baloo2 Fallback'; | ||||
|     font-style: normal; | ||||
|     font-weight: 400; | ||||
|     src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'), | ||||
|       local('Arial'); | ||||
|     ascent-override: 111.2%; | ||||
|     descent-override: 54.05%; | ||||
|     line-gap-override: 0%; | ||||
|     size-adjust: 96.95%; | ||||
|   strong { | ||||
|     @apply font-medium; | ||||
|   } | ||||
|  | ||||
|   @font-face { | ||||
|     font-family: 'Baloo2 Noto Fallback'; | ||||
|     font-style: normal; | ||||
|     font-weight: 400; | ||||
|     src: local('Noto Sans'); | ||||
|     ascent-override: 88%; | ||||
|     descent-override: none; | ||||
|     line-gap-override: 0%; | ||||
|     size-adjust: 92%; | ||||
|   } | ||||
| } | ||||
|  | ||||
| a { | ||||
|   @apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500; | ||||
| } | ||||
|  | ||||
| strong { | ||||
|   @apply font-medium; | ||||
| } | ||||
|  | ||||
| .article-body { | ||||
|   h1 { | ||||
|     @apply px-4 text-2xl font-semibold text-blue-900 mb-3 mt-4 max-w-read mx-auto md:text-4xl lg:text-5xl; | ||||
|   } | ||||
|  | ||||
|   h2 { | ||||
|     @apply px-4 text-xl font-semibold text-blue-900 mb-3 mt-4 max-w-read mx-auto md:text-2xl md:mb-6 md:mt-8 lg:mb-8 lg:mt-12 lg:text-4xl; | ||||
|   } | ||||
| @@ -82,15 +132,16 @@ strong { | ||||
|     @apply p-4; | ||||
|  | ||||
|     img { | ||||
|       @apply rounded shadow-md mx-auto lg:max-w-image; | ||||
|       @apply rounded-sm shadow-md mx-auto lg:max-w-image; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   figcaption { | ||||
|     @apply mt-2 text-center text-sm italic text-blue-800 md:text-base lg:text-lg; | ||||
|   } | ||||
|  | ||||
|   table { | ||||
|     @apply text-sm mx-auto my-4 max-w-image table-auto border-collapse border-spacing-12 border border-slate-200 rounded md:text-base lg:text-xl lg:my-8; | ||||
|     @apply text-sm mx-auto my-4 max-w-image table-auto border-collapse border-spacing-12 border border-slate-200 rounded-sm md:text-base lg:text-xl lg:my-8; | ||||
|   } | ||||
|  | ||||
|   thead { | ||||
| @@ -119,11 +170,11 @@ strong { | ||||
|   } | ||||
|  | ||||
|   :not(pre) code { | ||||
|     @apply text-pink-900 rounded border border-blue-300 px-1 py-0.5 bg-blue-100 text-sm md:text-base lg:text-xl; | ||||
|     @apply text-pink-900 rounded-sm border border-blue-300 px-1 py-0.5 bg-blue-100 text-sm md:text-base lg:text-xl; | ||||
|   } | ||||
|  | ||||
|   pre code pre { | ||||
|     @apply mx-2 rounded lg:mx-auto lg:text-lg shadow-sm lg:max-w-note; | ||||
|     @apply mx-2 rounded-sm lg:mx-auto lg:text-lg shadow-xs lg:max-w-note; | ||||
|   } | ||||
|  | ||||
|   ul, | ||||
| @@ -138,12 +189,13 @@ strong { | ||||
|   ul { | ||||
|     @apply list-disc; | ||||
|   } | ||||
|  | ||||
|   ol { | ||||
|     @apply list-decimal; | ||||
|   } | ||||
|  | ||||
|   iframe { | ||||
|     @apply rounded shadow-md mx-auto lg:max-w-image; | ||||
|     @apply rounded-sm shadow-md mx-auto lg:max-w-image; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -280,4 +332,4 @@ article a { | ||||
| /* animation-duration: 5.5s; */ | ||||
| /* transition: transform 5.4s ease-in-out; */ | ||||
| /* opacity: 1; */ | ||||
| /* } */ | ||||
| /* } */ | ||||
							
								
								
									
										3814
									
								
								styles/output.css
									
									
									
									
									
								
							
							
						
						
									
										3814
									
								
								styles/output.css
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,112 +0,0 @@ | ||||
| /** @type {import('tailwindcss').Config} */ | ||||
| module.exports = { | ||||
| 	content: ["./templates/**/**.html"], | ||||
| 	theme: { | ||||
| 		extend: { | ||||
| 			fontFamily: { | ||||
| 				sans: [ | ||||
| 					"Baloo2", | ||||
| 					"Baloo2 Noto Fallback", | ||||
| 					"Baloo2 Fallback", | ||||
| 					"ui-sans-serif", | ||||
| 					"system-ui", | ||||
| 					"sans-serif", | ||||
| 					"Apple Color Emoji", | ||||
| 					"Segoe UI Emoji", | ||||
| 					"Segoe UI Symbol", | ||||
| 					"Noto Color Emoji", | ||||
| 				], | ||||
| 			}, | ||||
| 			spacing: { | ||||
| 				note: "60rem", | ||||
| 				read: "64rem", | ||||
| 				image: "min(70rem, 95vw)", | ||||
| 				maxindex: "100rem", | ||||
| 			}, | ||||
| 			width: { | ||||
| 				note: "60rem", | ||||
| 				read: "64rem", | ||||
| 				image: "min(70rem, 95vw)", | ||||
| 				maxindex: "100rem", | ||||
| 			}, | ||||
| 			fontSize: { | ||||
| 				readxl: [ | ||||
| 					"1.75rem", | ||||
| 					{ | ||||
| 						lineHeight: "2.25rem", | ||||
| 						letterSpacing: "-0.015em", | ||||
| 						fontWeight: "400", | ||||
| 					}, | ||||
| 				], | ||||
| 			}, | ||||
| 			colors: { | ||||
| 				// blue: { | ||||
| 				// 	50: "#ecf6fe", | ||||
| 				// 	100: "#d9edfc", | ||||
| 				// 	200: "#b3dbf9", | ||||
| 				// 	300: "#8ecaf6", | ||||
| 				// 	400: "#68b8f3", | ||||
| 				// 	500: "#42a6f0", | ||||
| 				// 	600: "#3585c0", | ||||
| 				// 	700: "#286490", | ||||
| 				// 	800: "#1F4E71", | ||||
| 				// 	900: "#173A54", | ||||
| 				// 	950: "#0F2637", | ||||
| 				// }, | ||||
| 				blue: { | ||||
| 					50: "#f1f7fe", | ||||
| 					100: "#e1effd", | ||||
| 					200: "#bddefa", | ||||
| 					300: "#82c3f7", | ||||
| 					400: "#42a6f0", | ||||
| 					500: "#1789e0", | ||||
| 					600: "#0a6cbf", | ||||
| 					700: "#0a569a", | ||||
| 					800: "#0c4980", | ||||
| 					900: "#103e6a", | ||||
| 					950: "#0b2746", | ||||
| 				}, | ||||
| 				// pink: { | ||||
| 				// 	50: "#FFFBFE", | ||||
| 				// 	100: "#FFE4F9", | ||||
| 				// 	200: "#FECEF4", | ||||
| 				// 	300: "#FEB8EF", | ||||
| 				// 	400: "#fea6eb", | ||||
| 				// 	500: "#D38AC3", | ||||
| 				// 	600: "#B476A7", | ||||
| 				// 	700: "#96628B", | ||||
| 				// 	800: "#774E6E", | ||||
| 				// 	900: "#593A52", | ||||
| 				// 	950: "#3A2636", | ||||
| 				// }, | ||||
| 				pink: { | ||||
| 					50: "#fff4fd", | ||||
| 					100: "#ffe7fb", | ||||
| 					200: "#ffcff7", | ||||
| 					300: "#fea6eb", | ||||
| 					400: "#fc76dd", | ||||
| 					500: "#f342ca", | ||||
| 					600: "#d722a9", | ||||
| 					700: "#b31889", | ||||
| 					800: "#92166e", | ||||
| 					900: "#771859", | ||||
| 					950: "#500238", | ||||
| 				}, | ||||
| 				purple: { | ||||
| 					50: "#F8F5FC", | ||||
| 					100: "#D5C2ED", | ||||
| 					200: "#B28EDE", | ||||
| 					300: "#8F5BCF", | ||||
| 					400: "#6D30B9", | ||||
| 					500: "#5F2AA2", | ||||
| 					600: "#52248A", | ||||
| 					700: "#441E73", | ||||
| 					800: "#36185C", | ||||
| 					900: "#281244", | ||||
| 					950: "#1A0C2D", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	plugins: [], | ||||
| }; | ||||
| @@ -41,7 +41,7 @@ | ||||
| <section id="recommended-articles"> | ||||
|   <hr class="border-slate-300 m-5 md:my-8"> | ||||
|  | ||||
|   <h2 class="m-5 text-2xl md:text-2xl lg:text-4xl lg:mt-8 text-blue-900 lg:mb-10 font-bold">Further reading</h2> | ||||
|   <h2 class="m-5 mt-8 text-2xl md:text-2xl lg:text-4xl lg:mt-10 text-blue-900 lg:mb-10 font-bold">Further reading</h2> | ||||
|   <ul class="mx-5 xl:flex xl:justify-start xl:gap-10"> | ||||
|     {% for post in recommended_posts %} | ||||
|     <li class="flex-1"> | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
| 		  <a rel="prefetch" href="/{{segment}}/{{post.slug}}" class="text-blue-950 visited:text-purple-700 no-underline">{{post.metadata.title}}</a> | ||||
| 		</h3> | ||||
| 	</header> | ||||
| 	<section class="text-base leading-5 text-slate-800 md:text-xl text-justify">{{post.body|truncate_md(2)|parse_markdown|safe}}</section> | ||||
| 	<section class="text-base leading-7 text-slate-800 md:text-xl text-justify">{{post.body|truncate_md(2)|parse_markdown|safe}}</section> | ||||
| 	<footer class="text-sm md:text-base lg:text-lg mt-3 sm:mt-0 clear-both sm:clear-none"> | ||||
|     <ul class="inline-block" style="view-transition-name: post_tags_{{post.slug}}"> | ||||
|      {% for tag in post.metadata.tags %} | ||||
|   | ||||
| @@ -1,18 +0,0 @@ | ||||
| <section class="flex border rounded bg-white p-3"> | ||||
| 	<aside class="flex justify-center items-center pr-3 shrink-0"> | ||||
|   {% match education.thumbnail %} | ||||
|   {% when Some with (source) %} | ||||
|   {% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "education cover", Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %} | ||||
|   <figure class="mx-4 my-2"> | ||||
|      {{picture|safe}} | ||||
|   </figure> | ||||
|   {% when None %} | ||||
|   {% endmatch %} | ||||
| 	</aside> | ||||
| 	<section> | ||||
| 		<header> | ||||
| 			<h3 class="text-lg font-medium mb-1 md:text-2xl">{{education.name}}</h3> | ||||
| 		</header> | ||||
| 		<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{education.description|parse_markdown|safe}}</section> | ||||
| 	</section> | ||||
| </section> | ||||
| @@ -1,4 +1,4 @@ | ||||
| <section class="border rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};"> | ||||
| <section class="border border-slate-200 rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};"> | ||||
|   <header class="px-4 mb-3">  | ||||
|     <h2 class="text-xl font-semibold text-blue-900 md:text-2xl"> | ||||
|       {% match project.metadata.link %} | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| <section class="flex border rounded bg-white p-3"> | ||||
| <section class="flex border border-slate-200 rounded bg-white p-3"> | ||||
| 	<aside class="flex justify-center items-center pr-3 shrink-0"> | ||||
|   {% match skill.thumbnail %} | ||||
|   {% when Some with (source) %} | ||||
| 	{% let skill_name = skill.name.clone() %} | ||||
|   {% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, format!("{skill_name} cover"), Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %} | ||||
|   {% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, &format!("{} cover", skill.name), Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %} | ||||
|   <figure class="mx-4 my-2"> | ||||
|      {{picture|safe}} | ||||
|   </figure> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| {% macro social_card_start(svg, url, heading, img, class) %} | ||||
|  | ||||
| <a href="{{url}}" class="block no-underline border rounded-md bg-pink-200 m-4 p-4 max-w-[392px] {{class}}"> | ||||
| <a href="{{url}}" class="block no-underline border border-slate-200 rounded-md bg-pink-200 m-4 p-4 max-w-[392px] {{class}}"> | ||||
| 	<header class="flex text-center justify-center items-center gap-2 mb-2"> | ||||
| 		 <svg role="img" aria-label="{{svg}} icon" aria-hidden="true" class="h-7 w-7 fill-blue-950"> | ||||
| 	    <use href="#{{svg}}" /> | ||||
| @@ -9,6 +9,6 @@ | ||||
| 	</header> | ||||
| 	{% let alt_text = format!("{svg} thumbnail") %} | ||||
|  | ||||
| 	{{ crate::picture_generator::picture_markup_generator::generate_picture_markup(img, 360, 128, alt_text, Some("h-auto mx-auto rounded-sm")).unwrap_or("thumbnail not found".to_string())|safe }} | ||||
| 	{{ crate::picture_generator::picture_markup_generator::generate_picture_markup(img, 360, 128, alt_text, Some("h-auto mx-auto rounded-xs")).unwrap_or("thumbnail not found".to_string())|safe }} | ||||
| </a> | ||||
| {% endmacro %} | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| {% macro talent_card(svg, heading, description) %} | ||||
| <section class="flex border rounded bg-white p-3"> | ||||
| <section class="flex border border-slate-200 rounded-sm bg-white p-3"> | ||||
| 	<aside class="flex justify-center items-center pr-3"> | ||||
| 	  <svg role="img" aria-label="{{svg}} icon" aria-hidden="true" class="fill-blue-950 h-12 w-12 md:h-16 md:w-16"> | ||||
| 	    <use href="#{{svg}}" /> | ||||
|   | ||||
| @@ -1,18 +0,0 @@ | ||||
| <section class="flex border rounded bg-white p-3"> | ||||
| 	<aside class="flex justify-center items-center pr-3 shrink-0"> | ||||
|   {% match workplace.thumbnail %} | ||||
|   {% when Some with (source) %} | ||||
|   {% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "Workplace cover", Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %} | ||||
|   <figure class="mx-4 my-2"> | ||||
|      {{picture|safe}} | ||||
|   </figure> | ||||
|   {% when None %} | ||||
|   {% endmatch %} | ||||
| 	</aside> | ||||
| 	<section> | ||||
| 		<header> | ||||
| 			<h3 class="text-lg font-medium mb-1 md:text-2xl">{{workplace.name}}</h3> | ||||
| 		</header> | ||||
| 		<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{workplace.description|parse_markdown|safe}}</section> | ||||
| 	</section> | ||||
| </section> | ||||
| @@ -174,7 +174,7 @@ | ||||
|  | ||||
|     <ul class="m-6 flex gap-2 flex-wrap justify-center"> | ||||
|       {% for technology in technology_list %} | ||||
|       <li class="p-2 text-pink-900 bg-blue-100 text-sm border rounded border-blue-300 font-mono"> | ||||
|       <li class="p-2 text-pink-900 bg-blue-100 text-sm border rounded-sm border-blue-300 font-mono"> | ||||
|         {{technology}} | ||||
|       </li> | ||||
|       {% endfor %} | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
|     </a> | ||||
|     {% when None %} | ||||
|     {% endmatch %} | ||||
|       <aside class="flex logo-section flex-grow justify-end content-end"> | ||||
|       <aside class="flex logo-section grow justify-end content-end"> | ||||
|         <a class="logo p-3 text-base" href="/"> | ||||
|           @michalvankodev | ||||
|         </a> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user