Compare commits
19 Commits
5a1fb0c5f9
...
misc/migra
Author | SHA1 | Date | |
---|---|---|---|
6a5a9c890f | |||
5ba193cd56 | |||
95b105762d | |||
38e26ebf1f | |||
fa9b104f60 | |||
b47e8e18d0 | |||
546bf4400d | |||
134159f79c | |||
489156fe87 | |||
83a24557bd | |||
facb304a52 | |||
1e8f48b6fe | |||
1473b676f4 | |||
74f875460a | |||
cd639a830b | |||
cde3faa3c6 | |||
e8f9ecc241 | |||
1a059a005a | |||
ff087b0577 |
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"; }
|
||||
}
|
||||
}
|
||||
pane_template name="just" {
|
||||
command "just"
|
||||
start_suspended true
|
||||
}
|
||||
floating_panes {
|
||||
pane {
|
||||
command "just"
|
||||
args "tailwind"
|
||||
}
|
||||
pane {
|
||||
command "just"
|
||||
args "decap_server"
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
|
||||
tab split_direction="vertical" focus=true {
|
||||
pane {
|
||||
just { args "server_dev"; }
|
||||
just { args "test"; }
|
||||
}
|
||||
pane {
|
||||
just { args "tailwind"; }
|
||||
just { args "decap_server"; }
|
||||
}
|
||||
}
|
||||
tab
|
||||
}
|
||||
|
30
Cargo.toml
30
Cargo.toml
@ -6,11 +6,10 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.12", features = ["with-axum", "mime", "mime_guess"] }
|
||||
askama_axum = "0.4.0"
|
||||
axum = "0.7.3"
|
||||
askama = { version = "0.14" }
|
||||
axum = "0.8.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
pulldown-cmark = { version = "0.12" }
|
||||
pulldown-cmark = { version = "0.13" }
|
||||
gray_matter = "0.2.6"
|
||||
rss = "2.0.7"
|
||||
serde = "1.0.195"
|
||||
@ -25,25 +24,8 @@ anyhow = "1.0.86"
|
||||
rayon = "1.10.0"
|
||||
syntect = "5.2.0"
|
||||
indoc = "2.0.5"
|
||||
askama_escape = "0.10.3"
|
||||
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 {
|
||||
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,7 +52,8 @@ pub async fn render_blog_post_list(
|
||||
("Blog posts".to_string(), "Blog posts".to_string())
|
||||
};
|
||||
|
||||
Ok(PostListTemplate {
|
||||
Ok(Html(
|
||||
PostListTemplate {
|
||||
title,
|
||||
og_title,
|
||||
segment: Segment::Blog,
|
||||
@ -59,5 +62,8 @@ pub async fn render_blog_post_list(
|
||||
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,7 +60,8 @@ pub async fn render_blog_post(
|
||||
_ => HeaderProps::default(),
|
||||
};
|
||||
|
||||
Ok(BlogPostTemplate {
|
||||
Ok(Html(
|
||||
BlogPostTemplate {
|
||||
title: post.metadata.title,
|
||||
date: post.metadata.date,
|
||||
tags: post.metadata.tags,
|
||||
@ -69,7 +71,10 @@ pub async fn render_blog_post(
|
||||
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,7 +52,8 @@ pub async fn render_broadcast_post_list(
|
||||
"Broadcasts".to_string()
|
||||
};
|
||||
|
||||
Ok(PostListTemplate {
|
||||
Ok(Html(
|
||||
PostListTemplate {
|
||||
title: title.clone(),
|
||||
og_title: title,
|
||||
segment: Segment::Broadcasts,
|
||||
@ -59,5 +62,8 @@ pub async fn render_broadcast_post_list(
|
||||
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(),
|
||||
@ -44,6 +47,12 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
|
||||
title: "YouTube channel".to_string(),
|
||||
svg: "youtube".to_string(),
|
||||
},
|
||||
ContactLink {
|
||||
href: "https://mastodon.online/@michalvankodev".to_string(),
|
||||
label: "Mastodon".to_string(),
|
||||
title: "Mastodon profile".to_string(),
|
||||
svg: "mastodon".to_string(),
|
||||
},
|
||||
ContactLink {
|
||||
href: "https://instagram.com/michalvankodev".to_string(),
|
||||
label: "Instagram".to_string(),
|
||||
@ -70,9 +79,13 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
|
||||
},
|
||||
];
|
||||
|
||||
Ok(ContactPageTemplate {
|
||||
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 {
|
||||
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,
|
||||
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,7 +129,8 @@ pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
|
||||
.map(|str| str.to_owned())
|
||||
.collect();
|
||||
|
||||
Ok(PortfolioTemplate {
|
||||
Ok(Html(
|
||||
PortfolioTemplate {
|
||||
title: "Portfolio".to_owned(),
|
||||
body: portfolio.body,
|
||||
header_props: HeaderProps::default(),
|
||||
@ -135,5 +139,8 @@ pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
|
||||
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 {
|
||||
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 {
|
||||
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>
|
||||
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>"#,
|
||||
|
||||
>
|
||||
</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))
|
||||
|
1
static/svg/input/mastodon.svg
Normal file
1
static/svg/input/mastodon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"/></svg>
|
After Width: | Height: | Size: 811 B |
@ -1,18 +1,66 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
@font-face {
|
||||
@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 {
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Baloo 2';
|
||||
font-style: normal;
|
||||
font-weight: 400 800;
|
||||
@ -20,9 +68,9 @@
|
||||
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-face {
|
||||
font-family: 'Baloo2 Fallback';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@ -32,9 +80,9 @@
|
||||
descent-override: 54.05%;
|
||||
line-gap-override: 0%;
|
||||
size-adjust: 96.95%;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@font-face {
|
||||
font-family: 'Baloo2 Noto Fallback';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
@ -43,21 +91,23 @@
|
||||
descent-override: none;
|
||||
line-gap-override: 0%;
|
||||
size-adjust: 92%;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@layer base {
|
||||
a {
|
||||
@apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
3728
styles/output.css
3728
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 @@
|
||||
bgAnimationTimeline.add({
|
||||
targets: '#m-logo #bg-gradient #bg-stop',
|
||||
offset: "0%",
|
||||
stopColor: "rgba(216, 246, 255, 1)",
|
||||
stopColor: "rgba(216, 246, 255, 0.8)",
|
||||
easing: 'easeInQuint',
|
||||
duration: 123,
|
||||
})
|
||||
@ -50,6 +50,18 @@
|
||||
offset: "100%",
|
||||
easing: 'easeOutExpo',
|
||||
duration: 333,
|
||||
complete: (animation) => {
|
||||
const target = animation.animatables[0].target
|
||||
target.setAttribute("stop-color", "rgba(216, 246, 255, 0.8)")
|
||||
anime({
|
||||
targets: '#m-logo #bg-gradient #bg-stop',
|
||||
stopColor: "rgba(216, 246, 255, 0.3)",
|
||||
easing: 'easeOutQuad',
|
||||
duration: 3333,
|
||||
direction: 'alternate',
|
||||
loop: true,
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 160)
|
||||
|
@ -16,6 +16,10 @@
|
||||
href="/feed.xml"
|
||||
/>
|
||||
|
||||
<!-- Mastodon -->
|
||||
<link rel="me" href="https://mastodon.online/@michalvankodev" />
|
||||
<meta name="fediverse:creator" content="@michalvankodev@mastodon.online">
|
||||
|
||||
{% block og_meta %}
|
||||
<meta property="og:title" content="{% block title %} {{ title }} {% endblock %} @michalvankodev" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
@ -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>
|
@ -14,6 +14,6 @@
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</symbol><symbol id="linkedin" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/></symbol><symbol id="mail" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></symbol><symbol id="person-chalkboard" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M192 96a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm-8 384V352h16V480c0 17.7 14.3 32 32 32s32-14.3 32-32V192h56 64 16c17.7 0 32-14.3 32-32s-14.3-32-32-32H384V64H576V256H384V224H320v48c0 26.5 21.5 48 48 48H592c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48H368c-26.5 0-48 21.5-48 48v80H243.1 177.1c-33.7 0-64.9 17.7-82.3 46.6l-58.3 97c-9.1 15.1-4.2 34.8 10.9 43.9s34.8 4.2 43.9-10.9L120 256.9V480c0 17.7 14.3 32 32 32s32-14.3 32-32z"/></symbol><symbol id="phone" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M164.9 24.6c-7.7-18.6-28-28.5-47.4-23.2l-88 24C12.1 30.2 0 46 0 64C0 311.4 200.6 512 448 512c18 0 33.8-12.1 38.6-29.5l24-88c5.3-19.4-4.6-39.7-23.2-47.4l-96-40c-16.3-6.8-35.2-2.1-46.3 11.6L304.7 368C234.3 334.7 177.3 277.7 144 207.3L193.3 167c13.7-11.2 18.4-30 11.6-46.3l-40-96z"/></symbol><symbol id="rss" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></symbol><symbol id="tiktok-2" viewBox="0 0 24 24">
|
||||
</symbol><symbol id="linkedin" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/></symbol><symbol id="mail" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></symbol><symbol id="mastodon" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"/></symbol><symbol id="person-chalkboard" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M192 96a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm-8 384V352h16V480c0 17.7 14.3 32 32 32s32-14.3 32-32V192h56 64 16c17.7 0 32-14.3 32-32s-14.3-32-32-32H384V64H576V256H384V224H320v48c0 26.5 21.5 48 48 48H592c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48H368c-26.5 0-48 21.5-48 48v80H243.1 177.1c-33.7 0-64.9 17.7-82.3 46.6l-58.3 97c-9.1 15.1-4.2 34.8 10.9 43.9s34.8 4.2 43.9-10.9L120 256.9V480c0 17.7 14.3 32 32 32s32-14.3 32-32z"/></symbol><symbol id="phone" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M164.9 24.6c-7.7-18.6-28-28.5-47.4-23.2l-88 24C12.1 30.2 0 46 0 64C0 311.4 200.6 512 448 512c18 0 33.8-12.1 38.6-29.5l24-88c5.3-19.4-4.6-39.7-23.2-47.4l-96-40c-16.3-6.8-35.2-2.1-46.3 11.6L304.7 368C234.3 334.7 177.3 277.7 144 207.3L193.3 167c13.7-11.2 18.4-30 11.6-46.3l-40-96z"/></symbol><symbol id="rss" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></symbol><symbol id="tiktok-2" viewBox="0 0 24 24">
|
||||
<path d="M16.8217 5.1344C16.0886 4.29394 15.6479 3.19805 15.6479 2H14.7293M16.8217 5.1344C17.4898 5.90063 18.3944 6.45788 19.4245 6.67608C19.7446 6.74574 20.0786 6.78293 20.4266 6.78293V10.2191C18.645 10.2191 16.9932 9.64801 15.6477 8.68211V15.6707C15.6477 19.1627 12.8082 22 9.32386 22C7.50043 22 5.85334 21.2198 4.69806 19.98C3.64486 18.847 2.99994 17.3331 2.99994 15.6707C2.99994 12.2298 5.75592 9.42509 9.17073 9.35079M16.8217 5.1344C16.8039 5.12276 16.7861 5.11101 16.7684 5.09914M6.9855 17.3517C6.64217 16.8781 6.43802 16.2977 6.43802 15.6661C6.43802 14.0734 7.73249 12.7778 9.32394 12.7778C9.62087 12.7778 9.9085 12.8288 10.1776 12.9124V9.40192C9.89921 9.36473 9.61622 9.34149 9.32394 9.34149C9.27287 9.34149 8.86177 9.36884 8.81073 9.36884M14.7244 2H12.2097L12.2051 15.7775C12.1494 17.3192 10.8781 18.5591 9.32386 18.5591C8.35878 18.5591 7.50971 18.0808 6.98079 17.3564" stroke="#000000" stroke-linejoin="round"/>
|
||||
</symbol><symbol id="tiktok" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M448 209.9a210.1 210.1 0 0 1 -122.8-39.3V349.4A162.6 162.6 0 1 1 185 188.3V278.2a74.6 74.6 0 1 0 52.2 71.2V0l88 0a121.2 121.2 0 0 0 1.9 22.2h0A122.2 122.2 0 0 0 381 102.4a121.4 121.4 0 0 0 67 20.1z"/></symbol><symbol id="twitch" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M391.2 103.5H352.5v109.7h38.6zM285 103H246.4V212.8H285zM120.8 0 24.3 91.4V420.6H140.1V512l96.5-91.4h77.3L487.7 256V0zM449.1 237.8l-77.2 73.1H294.6l-67.6 64v-64H140.1V36.6H449.1z"/></symbol><symbol id="twitter" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></symbol><symbol id="youtube" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M549.7 124.1c-6.3-23.7-24.8-42.3-48.3-48.6C458.8 64 288 64 288 64S117.2 64 74.6 75.5c-23.5 6.3-42 24.9-48.3 48.6-11.4 42.9-11.4 132.3-11.4 132.3s0 89.4 11.4 132.3c6.3 23.7 24.8 41.5 48.3 47.8C117.2 448 288 448 288 448s170.8 0 213.4-11.5c23.5-6.3 42-24.2 48.3-47.8 11.4-42.9 11.4-132.3 11.4-132.3s0-89.4-11.4-132.3zm-317.5 213.5V175.2l142.7 81.2-142.7 81.2z"/></symbol></svg>
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
@ -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