Compare commits
2 Commits
main
...
5a1fb0c5f9
Author | SHA1 | Date | |
---|---|---|---|
5a1fb0c5f9 | |||
f09ce128e8 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -23,7 +23,3 @@ Cargo.lock
|
|||||||
|
|
||||||
# Image generator
|
# Image generator
|
||||||
generated_images/
|
generated_images/
|
||||||
|
|
||||||
node_modules/
|
|
||||||
package-lock.json
|
|
||||||
.aider*
|
|
||||||
|
@ -1,26 +1,29 @@
|
|||||||
layout {
|
layout {
|
||||||
default_tab_template {
|
pane split_direction="vertical" focus=true {
|
||||||
children
|
pane edit="src/main.rs"
|
||||||
pane size=2 borderless=true {
|
pane split_direction="horizontal" size=60 {
|
||||||
plugin location="zellij:status-bar"
|
just { args "server_dev"; }
|
||||||
}
|
just { args "test"; }
|
||||||
pane size=1 borderless=true {
|
|
||||||
plugin location="zellij:tab-bar"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pane_template name="just" {
|
pane_template name="just" {
|
||||||
command "just"
|
command "just"
|
||||||
|
start_suspended true
|
||||||
}
|
}
|
||||||
|
floating_panes {
|
||||||
tab split_direction="vertical" focus=true {
|
pane {
|
||||||
pane {
|
command "just"
|
||||||
just { args "server_dev"; }
|
args "tailwind"
|
||||||
just { args "test"; }
|
|
||||||
}
|
}
|
||||||
pane {
|
pane {
|
||||||
just { args "tailwind"; }
|
command "just"
|
||||||
just { args "decap_server"; }
|
args "decap_server"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
pane size=2 borderless=true {
|
||||||
|
plugin location="zellij:status-bar"
|
||||||
|
}
|
||||||
|
pane size=1 borderless=true {
|
||||||
|
plugin location="zellij:tab-bar"
|
||||||
}
|
}
|
||||||
tab
|
|
||||||
}
|
}
|
||||||
|
30
Cargo.toml
30
Cargo.toml
@ -6,10 +6,11 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
askama = { version = "0.14" }
|
askama = { version = "0.12", features = ["with-axum", "mime", "mime_guess"] }
|
||||||
axum = "0.8.0"
|
askama_axum = "0.4.0"
|
||||||
|
axum = "0.7.3"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
pulldown-cmark = { version = "0.13" }
|
pulldown-cmark = { version = "0.12" }
|
||||||
gray_matter = "0.2.6"
|
gray_matter = "0.2.6"
|
||||||
rss = "2.0.7"
|
rss = "2.0.7"
|
||||||
serde = "1.0.195"
|
serde = "1.0.195"
|
||||||
@ -24,8 +25,25 @@ anyhow = "1.0.86"
|
|||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
syntect = "5.2.0"
|
syntect = "5.2.0"
|
||||||
indoc = "2.0.5"
|
indoc = "2.0.5"
|
||||||
askama_escape = "0.13.0"
|
askama_escape = "0.10.3"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[build]
|
||||||
pretty_assertions = "1"
|
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
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
layout: blog
|
|
||||||
title: Somewhere in the sky
|
|
||||||
segments:
|
|
||||||
- blog
|
|
||||||
published: true
|
|
||||||
date: 2025-06-11T15:50:45.527Z
|
|
||||||
thumbnail: /images/uploads/dscf4399.jpg
|
|
||||||
tags:
|
|
||||||
- News
|
|
||||||
- Personal
|
|
||||||
- Travel
|
|
||||||
---
|
|
||||||
## Realization
|
|
||||||
|
|
||||||
What can you do on an airplane when you cannot move as your son is sleeping on your lap? You can **write a new blog post**, "obviously". Why can't I find more time to write more posts? I lack the motivation, to be honest. Now that anyone can just ask an LLM to spit out an original blog post on demand, who will search for original, honest content written by a human?
|
|
||||||
|
|
||||||
I've set expectations for my wife that this year we won't be going on a vacation, and she has overruled and found a one-week trip to Mallorca. I can confirm that we will be **coming back to Mallorca** for further exploration. This week was very nice. I haven't made any commitments on what I'm going to do with my career. I've already **accomplished** that since this year, **I will work on Rust projects**.
|
|
||||||
|
|
||||||
## Drift
|
|
||||||
|
|
||||||
In my latest vlog, I mentioned that I should talk more about burning out, and that I don't feel like I am burning out. Since then, I stopped streaming and stopped producing videos. **Did I jinx myself?** I don't know. Being active on social media is not something for me. I enjoy streaming, but I just had to put my family above my fulfillment. **I enjoy putting my son to sleep** much more. He is growing so fast. I expect that in a few years **we will start streaming together** as he explores the world of computers.
|
|
||||||
|
|
||||||
I expect to find **more time for streaming** when the sun falls in earlier hours.
|
|
||||||
|
|
||||||
You should've seen him. He was able to fill his bucket and pour it into a hole in the sand more than 100 times in a row. Where he finds the energy to do this is unbelievable. He is such an extraordinary individual.
|
|
||||||
|
|
||||||
## Inertia
|
|
||||||
|
|
||||||
*What about the game progress?*
|
|
||||||
|
|
||||||
**The game is in a terrible state**. I realized that the **whole animation system has to be rewritten** as it is unreliable. And that's almost everything that we have been working on. I've started to **write the game design document** so I don't lose my thoughts on mechanics and game setting. I should find more time to code as well, but currently, **I have just enough work and studies to keep me occupied**.
|
|
||||||
|
|
||||||
## Sight
|
|
||||||
|
|
||||||
Here are some photos from the trip:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Some of them are taken with my *Fuji X-Michal* and I have to say I've fallen in love with the machine. I wish some features were more accessible, and I hope with the new Fuji X-E5 announcement that the *Film simulation recipes* will be included in a future firmware upgrade. The current configuration is very misleading and not user-friendly.
|
|
||||||
|
|
||||||
I've also ordered a new Sigma 16-300, hoping it will be delivered before the vacation. I have already **been waiting for it for 2 months**, and still no indication of when it will arrive. I am pretty new to the hobby, and it looks like when ordering a new gear, there is a **guarantee of a long waiting time**, or it is out of stock pretty quickly.
|
|
||||||
|
|
||||||
I'd like to **post more pictures** here on the blog. I'm planning on releasing albums. I don't know if I should also make some changes to the visualization, or if the current blog page rendering system is fine as it is. **Let me know if you have some cool ideas**.
|
|
||||||
|
|
||||||
I'm going to keep it short. We've landed successfully. The vacation was amazing. We are definitely coming back to Mallorca. Expect more content (soon).
|
|
||||||
|
|
||||||
Happy summer.
|
|
||||||
|
|
2
justfile
2
justfile
@ -2,7 +2,7 @@ port := env_var_or_default('PORT', '3080')
|
|||||||
|
|
||||||
# Tailwind in watch mode
|
# Tailwind in watch mode
|
||||||
tailwind:
|
tailwind:
|
||||||
npx @tailwindcss/cli -i ./styles/input.css -o ./styles/output.css --watch
|
npx tailwindcss -i ./styles/input.css -o ./styles/output.css --watch
|
||||||
|
|
||||||
# svg sprite creation
|
# svg sprite creation
|
||||||
svgstore:
|
svgstore:
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"@tailwindcss/cli": "^4.1.10",
|
|
||||||
"tailwindcss": "^4.1.10"
|
|
||||||
}
|
|
||||||
}
|
|
17
src/feed.rs
17
src/feed.rs
@ -1,4 +1,3 @@
|
|||||||
use askama::Values;
|
|
||||||
use axum::http::{header, StatusCode};
|
use axum::http::{header, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
@ -8,14 +7,6 @@ use crate::blog_posts::blog_post_model::{BlogPostMetadata, BLOG_POST_PATH};
|
|||||||
use crate::filters::{parse_markdown, truncate_md};
|
use crate::filters::{parse_markdown, truncate_md};
|
||||||
use crate::post_utils::post_listing::get_post_list;
|
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> {
|
pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
|
||||||
let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
||||||
.await
|
.await
|
||||||
@ -36,14 +27,14 @@ pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
|
|||||||
.title(Some(post.metadata.title))
|
.title(Some(post.metadata.title))
|
||||||
.link(Some(format!("https://michalvanko.dev/blog/{}", post.slug)))
|
.link(Some(format!("https://michalvanko.dev/blog/{}", post.slug)))
|
||||||
.description({
|
.description({
|
||||||
let truncated = truncate_md(&post.body, &EmptyValues, 2)
|
let truncated =
|
||||||
.unwrap_or("Can't parse post body".to_string());
|
truncate_md(&post.body, 2).unwrap_or("Can't parse post body".to_string());
|
||||||
let parsed_md = parse_markdown(&truncated, &EmptyValues)
|
let parsed_md = parse_markdown(&truncated)
|
||||||
.unwrap_or("Can't process truncated post body".to_string());
|
.unwrap_or("Can't process truncated post body".to_string());
|
||||||
Some(parsed_md)
|
Some(parsed_md)
|
||||||
})
|
})
|
||||||
.content({
|
.content({
|
||||||
let parsed_md = parse_markdown(&post.body, &EmptyValues)
|
let parsed_md = parse_markdown(&post.body)
|
||||||
.unwrap_or("Can't process full post body".to_string());
|
.unwrap_or("Can't process full post body".to_string());
|
||||||
Some(parsed_md)
|
Some(parsed_md)
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use core::fmt;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use image::image_dimensions;
|
use image::image_dimensions;
|
||||||
@ -20,10 +19,7 @@ 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>(
|
pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> {
|
||||||
markdown: T,
|
|
||||||
_: &dyn askama::Values,
|
|
||||||
) -> ::askama::Result<String> {
|
|
||||||
let mut options = Options::empty();
|
let mut options = Options::empty();
|
||||||
options.insert(Options::ENABLE_TABLES);
|
options.insert(Options::ENABLE_TABLES);
|
||||||
options.insert(Options::ENABLE_FOOTNOTES);
|
options.insert(Options::ENABLE_FOOTNOTES);
|
||||||
@ -38,8 +34,7 @@ pub fn parse_markdown<T: fmt::Display>(
|
|||||||
let theme = theme_set.themes.get("InspiredGitHub").unwrap();
|
let theme = theme_set.themes.get("InspiredGitHub").unwrap();
|
||||||
let mut heading_ended: Option<bool> = None;
|
let mut heading_ended: Option<bool> = None;
|
||||||
|
|
||||||
let mds = markdown.to_string();
|
let parser = Parser::new_ext(markdown, options).map(|event| match event {
|
||||||
let parser = Parser::new_ext(&mds, options).map(|event| match event {
|
|
||||||
/*
|
/*
|
||||||
Parsing images considers `alt` attribute as inner `Text` event
|
Parsing images considers `alt` attribute as inner `Text` event
|
||||||
Therefore the `[alt]` is rendered in html as subtitle
|
Therefore the `[alt]` is rendered in html as subtitle
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
// This filter does not have extra arguments
|
// This filter does not have extra arguments
|
||||||
pub fn pretty_date(date_time: &DateTime<Utc>, _: &dyn askama::Values) -> ::askama::Result<String> {
|
pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> {
|
||||||
let formatted = format!("{}", date_time.format("%e %B %Y"));
|
let formatted = format!("{}", date_time.format("%e %B %Y"));
|
||||||
Ok(formatted)
|
Ok(formatted)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"];
|
const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"];
|
||||||
|
|
||||||
pub fn truncate_md(body: &str, _: &dyn askama::Values, rows: usize) -> ::askama::Result<String> {
|
pub fn truncate_md(body: &str, rows: usize) -> ::askama::Result<String> {
|
||||||
let description = body
|
let description = body
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|line| {
|
.filter(|line| {
|
||||||
|
@ -1,17 +1,9 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "admin.html")]
|
#[template(path = "admin.html")]
|
||||||
pub struct AdminPageTemplate {}
|
pub struct AdminPageTemplate {}
|
||||||
|
|
||||||
pub async fn render_admin() -> Result<impl IntoResponse, StatusCode> {
|
pub async fn render_admin() -> AdminPageTemplate {
|
||||||
Ok(Html(
|
AdminPageTemplate {}
|
||||||
AdminPageTemplate {}
|
|
||||||
.render()
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::http::StatusCode;
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "assets/animated_logo.html")]
|
#[template(path = "assets/animated_logo.html")]
|
||||||
pub struct AnimatedLogoTemplate {}
|
pub struct AnimatedLogoTemplate {}
|
||||||
|
|
||||||
pub async fn render_animated_logo() -> Result<impl IntoResponse, StatusCode> {
|
pub async fn render_animated_logo() -> Result<AnimatedLogoTemplate, StatusCode> {
|
||||||
Ok(Html(AnimatedLogoTemplate {}.render().unwrap()))
|
Ok(AnimatedLogoTemplate {})
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use askama::Template;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{OriginalUri, Path},
|
extract::{OriginalUri, Path},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
};
|
||||||
use tokio::try_join;
|
use tokio::try_join;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@ -24,7 +22,7 @@ use super::post_list::PostListTemplate;
|
|||||||
pub async fn render_blog_post_list(
|
pub async fn render_blog_post_list(
|
||||||
tag: Option<Path<String>>,
|
tag: Option<Path<String>>,
|
||||||
OriginalUri(original_uri): OriginalUri,
|
OriginalUri(original_uri): OriginalUri,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<PostListTemplate, StatusCode> {
|
||||||
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
||||||
let tag = tag.map(|Path(tag)| tag);
|
let tag = tag.map(|Path(tag)| tag);
|
||||||
|
|
||||||
@ -52,18 +50,14 @@ pub async fn render_blog_post_list(
|
|||||||
("Blog posts".to_string(), "Blog posts".to_string())
|
("Blog posts".to_string(), "Blog posts".to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Html(
|
Ok(PostListTemplate {
|
||||||
PostListTemplate {
|
title,
|
||||||
title,
|
og_title,
|
||||||
og_title,
|
segment: Segment::Blog,
|
||||||
segment: Segment::Blog,
|
posts,
|
||||||
posts,
|
header_props,
|
||||||
header_props,
|
tags: blog_tags,
|
||||||
tags: blog_tags,
|
featured_projects,
|
||||||
featured_projects,
|
current_url: original_uri.to_string(),
|
||||||
current_url: original_uri.to_string(),
|
})
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::extract::OriginalUri;
|
use axum::extract::OriginalUri;
|
||||||
use axum::response::{Html, IntoResponse};
|
|
||||||
use axum::{extract::Path, http::StatusCode};
|
use axum::{extract::Path, http::StatusCode};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
@ -31,7 +30,7 @@ pub struct BlogPostTemplate {
|
|||||||
pub async fn render_blog_post(
|
pub async fn render_blog_post(
|
||||||
Path(post_id): Path<String>,
|
Path(post_id): Path<String>,
|
||||||
OriginalUri(original_uri): OriginalUri,
|
OriginalUri(original_uri): OriginalUri,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<BlogPostTemplate, StatusCode> {
|
||||||
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
|
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
|
||||||
let post = parse_post::<BlogPostMetadata>(&path).await?;
|
let post = parse_post::<BlogPostMetadata>(&path).await?;
|
||||||
let segment = if original_uri.to_string().starts_with("/blog") {
|
let segment = if original_uri.to_string().starts_with("/blog") {
|
||||||
@ -60,21 +59,17 @@ pub async fn render_blog_post(
|
|||||||
_ => HeaderProps::default(),
|
_ => HeaderProps::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Html(
|
Ok(BlogPostTemplate {
|
||||||
BlogPostTemplate {
|
title: post.metadata.title,
|
||||||
title: post.metadata.title,
|
date: post.metadata.date,
|
||||||
date: post.metadata.date,
|
tags: post.metadata.tags,
|
||||||
tags: post.metadata.tags,
|
body: post.body,
|
||||||
body: post.body,
|
slug: post.slug,
|
||||||
slug: post.slug,
|
segment,
|
||||||
segment,
|
thumbnail: post.metadata.thumbnail,
|
||||||
thumbnail: post.metadata.thumbnail,
|
header_props,
|
||||||
header_props,
|
recommended_posts,
|
||||||
recommended_posts,
|
})
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_recommended_posts(
|
async fn get_recommended_posts(
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use askama::Template;
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{OriginalUri, Path},
|
extract::{OriginalUri, Path},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
};
|
||||||
use tokio::try_join;
|
use tokio::try_join;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
@ -23,7 +21,7 @@ use super::post_list::PostListTemplate;
|
|||||||
pub async fn render_broadcast_post_list(
|
pub async fn render_broadcast_post_list(
|
||||||
tag: Option<Path<String>>,
|
tag: Option<Path<String>>,
|
||||||
OriginalUri(original_uri): OriginalUri,
|
OriginalUri(original_uri): OriginalUri,
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
) -> Result<PostListTemplate, StatusCode> {
|
||||||
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
||||||
let tag = tag.map(|Path(tag)| tag);
|
let tag = tag.map(|Path(tag)| tag);
|
||||||
|
|
||||||
@ -52,18 +50,14 @@ pub async fn render_broadcast_post_list(
|
|||||||
"Broadcasts".to_string()
|
"Broadcasts".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Html(
|
Ok(PostListTemplate {
|
||||||
PostListTemplate {
|
title: title.clone(),
|
||||||
title: title.clone(),
|
og_title: title,
|
||||||
og_title: title,
|
segment: Segment::Broadcasts,
|
||||||
segment: Segment::Broadcasts,
|
posts,
|
||||||
posts,
|
header_props,
|
||||||
header_props,
|
tags: popular_tags,
|
||||||
tags: popular_tags,
|
featured_projects,
|
||||||
featured_projects,
|
current_url: original_uri.to_string(),
|
||||||
current_url: original_uri.to_string(),
|
})
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::http::StatusCode;
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::components::site_header::HeaderProps;
|
use crate::components::site_header::HeaderProps;
|
||||||
|
|
||||||
@ -21,7 +18,7 @@ pub struct ContactPageTemplate {
|
|||||||
pub links: Vec<ContactLink>,
|
pub links: Vec<ContactLink>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_contact() -> Result<impl IntoResponse, StatusCode> {
|
pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
|
||||||
let links = vec![
|
let links = vec![
|
||||||
ContactLink {
|
ContactLink {
|
||||||
href: "mailto:michalvankosk@gmail.com".to_string(),
|
href: "mailto:michalvankosk@gmail.com".to_string(),
|
||||||
@ -79,13 +76,9 @@ pub async fn render_contact() -> Result<impl IntoResponse, StatusCode> {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
Ok(Html(
|
Ok(ContactPageTemplate {
|
||||||
ContactPageTemplate {
|
title: "Contact".to_owned(),
|
||||||
title: "Contact".to_owned(),
|
header_props: HeaderProps::default(),
|
||||||
header_props: HeaderProps::default(),
|
links,
|
||||||
links,
|
})
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::http::StatusCode;
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
use tokio::try_join;
|
use tokio::try_join;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -29,7 +26,7 @@ pub struct IndexTemplate {
|
|||||||
featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>,
|
featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_index() -> Result<impl IntoResponse, StatusCode> {
|
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
||||||
let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!(
|
let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!(
|
||||||
get_popular_tags(Some(Segment::Blog)),
|
get_popular_tags(Some(Segment::Blog)),
|
||||||
get_popular_tags(Some(Segment::Broadcasts)),
|
get_popular_tags(Some(Segment::Broadcasts)),
|
||||||
@ -47,16 +44,12 @@ pub async fn render_index() -> Result<impl IntoResponse, StatusCode> {
|
|||||||
let featured_broadcasts =
|
let featured_broadcasts =
|
||||||
ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]);
|
ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]);
|
||||||
|
|
||||||
Ok(Html(
|
Ok(IndexTemplate {
|
||||||
IndexTemplate {
|
header_props: HeaderProps::default(),
|
||||||
header_props: HeaderProps::default(),
|
blog_tags,
|
||||||
blog_tags,
|
broadcasts_tags,
|
||||||
broadcasts_tags,
|
featured_blog_posts,
|
||||||
featured_blog_posts,
|
featured_projects,
|
||||||
featured_projects,
|
featured_broadcasts,
|
||||||
featured_broadcasts,
|
})
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{extract::OriginalUri, http::StatusCode};
|
||||||
extract::OriginalUri,
|
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::components::site_header::HeaderProps;
|
use crate::components::site_header::HeaderProps;
|
||||||
@ -17,17 +13,13 @@ pub struct NotFoundPage {
|
|||||||
|
|
||||||
pub async fn render_not_found(
|
pub async fn render_not_found(
|
||||||
OriginalUri(original_uri): OriginalUri,
|
OriginalUri(original_uri): OriginalUri,
|
||||||
) -> Result<(StatusCode, impl IntoResponse), StatusCode> {
|
) -> Result<(StatusCode, NotFoundPage), StatusCode> {
|
||||||
info!("{original_uri} not found");
|
info!("{original_uri} not found");
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
Html(
|
NotFoundPage {
|
||||||
NotFoundPage {
|
title: "This page does not exists".to_owned(),
|
||||||
title: "This page does not exists".to_owned(),
|
header_props: HeaderProps::default(),
|
||||||
header_props: HeaderProps::default(),
|
},
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::http::StatusCode;
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -53,7 +50,7 @@ pub struct PortfolioTemplate {
|
|||||||
pub technology_list: Vec<String>,
|
pub technology_list: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_portfolio() -> Result<impl IntoResponse, StatusCode> {
|
pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
|
||||||
let portfolio = parse_post::<PortfolioPageModel>("_pages/portfolio.md").await?;
|
let portfolio = parse_post::<PortfolioPageModel>("_pages/portfolio.md").await?;
|
||||||
|
|
||||||
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
|
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
|
||||||
@ -129,18 +126,14 @@ pub async fn render_portfolio() -> Result<impl IntoResponse, StatusCode> {
|
|||||||
.map(|str| str.to_owned())
|
.map(|str| str.to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Html(
|
Ok(PortfolioTemplate {
|
||||||
PortfolioTemplate {
|
title: "Portfolio".to_owned(),
|
||||||
title: "Portfolio".to_owned(),
|
body: portfolio.body,
|
||||||
body: portfolio.body,
|
header_props: HeaderProps::default(),
|
||||||
header_props: HeaderProps::default(),
|
project_list,
|
||||||
project_list,
|
workplace_list,
|
||||||
workplace_list,
|
education_list,
|
||||||
education_list,
|
contact_links,
|
||||||
contact_links,
|
technology_list,
|
||||||
technology_list,
|
})
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::http::StatusCode;
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::site_header::HeaderProps,
|
components::site_header::HeaderProps,
|
||||||
@ -19,20 +16,16 @@ pub struct ProjectListTemplate {
|
|||||||
pub header_props: HeaderProps,
|
pub header_props: HeaderProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_projects_list() -> Result<impl IntoResponse, StatusCode> {
|
pub async fn render_projects_list() -> Result<ProjectListTemplate, StatusCode> {
|
||||||
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
|
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
|
||||||
|
|
||||||
project_list.sort_by_key(|post| post.slug.to_string());
|
project_list.sort_by_key(|post| post.slug.to_string());
|
||||||
project_list.retain(|project| project.metadata.displayed);
|
project_list.retain(|project| project.metadata.displayed);
|
||||||
project_list.reverse();
|
project_list.reverse();
|
||||||
|
|
||||||
Ok(Html(
|
Ok(ProjectListTemplate {
|
||||||
ProjectListTemplate {
|
title: "Showcase".to_owned(),
|
||||||
title: "Showcase".to_owned(),
|
header_props: HeaderProps::default(),
|
||||||
header_props: HeaderProps::default(),
|
project_list,
|
||||||
project_list,
|
})
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::http::StatusCode;
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::components::site_header::HeaderProps;
|
use crate::components::site_header::HeaderProps;
|
||||||
|
|
||||||
@ -12,12 +9,8 @@ pub struct EggFetcherShowcaseTemplate {
|
|||||||
header_props: HeaderProps,
|
header_props: HeaderProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_egg_fetcher() -> Result<impl IntoResponse, StatusCode> {
|
pub async fn render_egg_fetcher() -> Result<EggFetcherShowcaseTemplate, StatusCode> {
|
||||||
Ok(Html(
|
Ok(EggFetcherShowcaseTemplate {
|
||||||
EggFetcherShowcaseTemplate {
|
header_props: HeaderProps::default(),
|
||||||
header_props: HeaderProps::default(),
|
})
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ use super::export_format::ExportFormat;
|
|||||||
|
|
||||||
pub fn generate_images(
|
pub fn generate_images(
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
disk_image_path: &Path,
|
|
||||||
path_to_generated: &Path,
|
path_to_generated: &Path,
|
||||||
resolutions: &[(u32, u32, f32)],
|
resolutions: &[(u32, u32, f32)],
|
||||||
formats: &[ExportFormat],
|
formats: &[ExportFormat],
|
||||||
@ -39,9 +38,8 @@ pub fn generate_images(
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to generate {:?} - {:?}", &save_path, err);
|
error!("Failed to generate {:?} - {:?}", &save_path, err);
|
||||||
}
|
}
|
||||||
Ok(_) => {
|
_ => {
|
||||||
debug!("Generated image {:?}", &save_path);
|
debug!("Generated image {:?}", &save_path);
|
||||||
let _ = copy_exif(disk_image_path, &save_path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -49,22 +47,3 @@ pub fn generate_images(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_exif(orig_path: &Path, save_path: &Path) -> Result<(), anyhow::Error> {
|
|
||||||
let status = std::process::Command::new("exiftool")
|
|
||||||
.args([
|
|
||||||
"-TagsFromFile",
|
|
||||||
orig_path.to_str().expect("Orig path should exist"),
|
|
||||||
"-exif:all",
|
|
||||||
"-overwrite_original",
|
|
||||||
save_path.to_str().expect("Save path of image should exist"),
|
|
||||||
])
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
if status.success() {
|
|
||||||
debug!("EXIF copied successfully.");
|
|
||||||
} else {
|
|
||||||
error!("Failed to copy EXIF.");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@ -14,8 +14,7 @@ pub fn generate_image_with_src(
|
|||||||
height: u32,
|
height: u32,
|
||||||
suffix: &str,
|
suffix: &str,
|
||||||
) -> Result<String, anyhow::Error> {
|
) -> Result<String, anyhow::Error> {
|
||||||
let orig_path = Path::new(orig_img_path);
|
let path_to_generated = get_generated_file_name(orig_img_path);
|
||||||
let path_to_generated = get_generated_file_name(orig_path);
|
|
||||||
let file_stem = path_to_generated.file_stem().unwrap().to_str().unwrap();
|
let file_stem = path_to_generated.file_stem().unwrap().to_str().unwrap();
|
||||||
let path_to_generated = path_to_generated.with_file_name(format!("{file_stem}{suffix}"));
|
let path_to_generated = path_to_generated.with_file_name(format!("{file_stem}{suffix}"));
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ pub fn generate_image_with_src(
|
|||||||
Path::new("static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
|
Path::new("static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
|
||||||
let resolutions = [(width, height, 1.)];
|
let resolutions = [(width, height, 1.)];
|
||||||
|
|
||||||
let exported_formats = get_export_formats(orig_path);
|
let exported_formats = get_export_formats(orig_img_path);
|
||||||
|
|
||||||
if exported_formats.is_empty() {
|
if exported_formats.is_empty() {
|
||||||
return Ok(orig_img_path.to_string());
|
return Ok(orig_img_path.to_string());
|
||||||
@ -40,11 +39,11 @@ pub fn generate_image_with_src(
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.decode()
|
.decode()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let path_to_generated = path_to_generated_clone.as_ref();
|
||||||
|
|
||||||
let result = generate_images(
|
let result = generate_images(
|
||||||
&orig_img,
|
&orig_img,
|
||||||
&disk_img_path,
|
path_to_generated,
|
||||||
path_to_generated_clone.as_ref(),
|
|
||||||
&resolutions,
|
&resolutions,
|
||||||
&[exported_format],
|
&[exported_format],
|
||||||
)
|
)
|
||||||
|
@ -21,8 +21,7 @@ pub fn generate_picture_markup(
|
|||||||
alt_text: &str,
|
alt_text: &str,
|
||||||
class_name: Option<&str>,
|
class_name: Option<&str>,
|
||||||
) -> Result<String, anyhow::Error> {
|
) -> Result<String, anyhow::Error> {
|
||||||
let orig_path = Path::new(orig_img_path);
|
let exported_formats = get_export_formats(orig_img_path);
|
||||||
let exported_formats = get_export_formats(orig_path);
|
|
||||||
let class_attr = if let Some(class) = class_name {
|
let class_attr = if let Some(class) = class_name {
|
||||||
format!(r#"class="{class}""#)
|
format!(r#"class="{class}""#)
|
||||||
} else {
|
} else {
|
||||||
@ -40,13 +39,14 @@ pub fn generate_picture_markup(
|
|||||||
>"#
|
>"#
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let path_to_generated = get_generated_file_name(orig_path);
|
let path_to_generated = get_generated_file_name(orig_img_path);
|
||||||
|
|
||||||
let disk_img_path =
|
let disk_img_path =
|
||||||
Path::new("static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
|
Path::new("static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
|
||||||
|
|
||||||
let orig_img_dimensions = image_dimensions(&disk_img_path)?;
|
let orig_img_dimensions = image_dimensions(&disk_img_path)?;
|
||||||
let resolutions = get_resolutions(orig_img_dimensions, width, height);
|
let resolutions = get_resolutions(orig_img_dimensions, width, height);
|
||||||
|
|
||||||
let path_to_generated_arc = Arc::new(path_to_generated);
|
let path_to_generated_arc = Arc::new(path_to_generated);
|
||||||
let path_to_generated_clone = Arc::clone(&path_to_generated_arc);
|
let path_to_generated_clone = Arc::clone(&path_to_generated_arc);
|
||||||
let resolutions_arc = Arc::new(resolutions);
|
let resolutions_arc = Arc::new(resolutions);
|
||||||
@ -54,7 +54,6 @@ pub fn generate_picture_markup(
|
|||||||
let exported_formats_arc = Arc::new(exported_formats);
|
let exported_formats_arc = Arc::new(exported_formats);
|
||||||
let exported_formats_clone = Arc::clone(&exported_formats_arc);
|
let exported_formats_clone = Arc::clone(&exported_formats_arc);
|
||||||
|
|
||||||
// AI? Which data escapes?
|
|
||||||
rayon::spawn(move || {
|
rayon::spawn(move || {
|
||||||
let orig_img = ImageReader::open(&disk_img_path)
|
let orig_img = ImageReader::open(&disk_img_path)
|
||||||
.with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path))
|
.with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path))
|
||||||
@ -65,14 +64,8 @@ pub fn generate_picture_markup(
|
|||||||
let resolutions = resolutions_clone.as_ref();
|
let resolutions = resolutions_clone.as_ref();
|
||||||
let exported_formats = exported_formats_clone.as_ref();
|
let exported_formats = exported_formats_clone.as_ref();
|
||||||
|
|
||||||
let result = generate_images(
|
let result = generate_images(&orig_img, path_to_generated, resolutions, exported_formats)
|
||||||
&orig_img,
|
.with_context(|| "Failed to generate images".to_string());
|
||||||
&disk_img_path,
|
|
||||||
path_to_generated,
|
|
||||||
resolutions,
|
|
||||||
exported_formats,
|
|
||||||
)
|
|
||||||
.with_context(|| "Failed to generate images".to_string());
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
tracing::error!("Error: {}", e);
|
tracing::error!("Error: {}", e);
|
||||||
}
|
}
|
||||||
@ -200,9 +193,15 @@ fn strip_prefixes(path: &Path) -> &Path {
|
|||||||
parent_path
|
parent_path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_generated_file_name(orig_img_path: &Path) -> PathBuf {
|
pub fn get_generated_file_name(orig_img_path: &str) -> PathBuf {
|
||||||
let parent = strip_prefixes(orig_img_path);
|
let path = Path::new(&orig_img_path);
|
||||||
let file_name = orig_img_path
|
// let parent = path
|
||||||
|
// .parent()
|
||||||
|
// .expect("There should be a parent route to an image")
|
||||||
|
// .strip_prefix(".")
|
||||||
|
// .unwrap();
|
||||||
|
let parent = strip_prefixes(path);
|
||||||
|
let file_name = path
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.expect("There should be a name for every img");
|
.expect("There should be a name for every img");
|
||||||
let result = Path::new("/generated_images/")
|
let result = Path::new("/generated_images/")
|
||||||
@ -217,7 +216,7 @@ pub fn get_generated_file_name(orig_img_path: &Path) -> PathBuf {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_generated_paths() {
|
fn test_get_generated_paths() {
|
||||||
let orig_img_path = Path::new("/images/uploads/img_name.jpg");
|
let orig_img_path = "/images/uploads/img_name.jpg";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_generated_file_name(orig_img_path)
|
get_generated_file_name(orig_img_path)
|
||||||
.to_str()
|
.to_str()
|
||||||
@ -234,9 +233,9 @@ fn generate_srcset(path: &Path, format: &ExportFormat, resolutions: &[(u32, u32,
|
|||||||
let (width, height, density) = resolution;
|
let (width, height, density) = resolution;
|
||||||
let path_name = path.to_str().expect("Path to an image has to be valid");
|
let path_name = path.to_str().expect("Path to an image has to be valid");
|
||||||
let formatted_density = if density.fract() == 0.0 {
|
let formatted_density = if density.fract() == 0.0 {
|
||||||
format!("{density}") // Convert to integer if there is no decimal part
|
format!("{}", density) // Convert to integer if there is no decimal part
|
||||||
} else {
|
} else {
|
||||||
format!("{density:.1}") // Format to 1 decimal place if there is a fractional part
|
format!("{:.1}", density) // Format to 1 decimal place if there is a fractional part
|
||||||
};
|
};
|
||||||
format!("{path_name}_{width}x{height}.{extension} {formatted_density}x")
|
format!("{path_name}_{width}x{height}.{extension} {formatted_density}x")
|
||||||
})
|
})
|
||||||
@ -244,8 +243,10 @@ fn generate_srcset(path: &Path, format: &ExportFormat, resolutions: &[(u32, u32,
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_export_formats(orig_img_path: &Path) -> Vec<ExportFormat> {
|
pub fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> {
|
||||||
let path = orig_img_path.extension().and_then(|ext| ext.to_str());
|
let path = Path::new(&orig_img_path)
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str());
|
||||||
|
|
||||||
match path {
|
match path {
|
||||||
// THINK: Do we want to enable avif? It's very expensive to encode
|
// THINK: Do we want to enable avif? It's very expensive to encode
|
||||||
@ -260,9 +261,8 @@ pub fn get_export_formats(orig_img_path: &Path) -> Vec<ExportFormat> {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_get_export_formats() {
|
fn test_get_export_formats() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_export_formats(Path::new("/images/uploads/img_name.jpg")),
|
get_export_formats("/images/uploads/img_name.jpg"),
|
||||||
// vec![ExportFormat::Avif, ExportFormat::Jpeg]
|
vec![ExportFormat::Avif, ExportFormat::Jpeg]
|
||||||
vec![ExportFormat::Jpeg]
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
@ -292,21 +292,24 @@ fn test_generate_picture_markup() {
|
|||||||
let height = 200;
|
let height = 200;
|
||||||
let orig_img_path = "/images/uploads/2020-03-23_20-24-06_393.jpg";
|
let orig_img_path = "/images/uploads/2020-03-23_20-24-06_393.jpg";
|
||||||
let result = indoc! {
|
let result = indoc! {
|
||||||
r#"<picture>
|
r#"<picture>
|
||||||
<source
|
<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"
|
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/jpeg"
|
type="image/avif"
|
||||||
>
|
>
|
||||||
<img
|
<source
|
||||||
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
|
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"
|
||||||
width="300"
|
type="image/jpeg"
|
||||||
height="200"
|
>
|
||||||
alt="Testing image alt"
|
<img
|
||||||
|
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
|
||||||
>
|
width="300"
|
||||||
</picture>"#,
|
height="200"
|
||||||
|
alt="Testing image alt"
|
||||||
|
>
|
||||||
|
</picture>"#,
|
||||||
};
|
};
|
||||||
pretty_assertions::assert_eq!(
|
assert_eq!(
|
||||||
generate_picture_markup(orig_img_path, width, height, "Testing image alt", None,)
|
generate_picture_markup(orig_img_path, width, height, "Testing image alt", None,)
|
||||||
.expect("picture markup has to be generated"),
|
.expect("picture markup has to be generated"),
|
||||||
result
|
result
|
||||||
|
@ -8,14 +8,7 @@ use crate::{
|
|||||||
project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher,
|
project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use askama::Template;
|
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
|
||||||
use axum::{
|
|
||||||
extract::MatchedPath,
|
|
||||||
http::{Request, StatusCode},
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::get,
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::info_span;
|
use tracing::info_span;
|
||||||
|
|
||||||
@ -23,15 +16,15 @@ pub fn get_router() -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(render_index))
|
.route("/", get(render_index))
|
||||||
.route("/blog", get(render_blog_post_list))
|
.route("/blog", get(render_blog_post_list))
|
||||||
.route("/blog/tags/{tag}", get(render_blog_post_list))
|
.route("/blog/tags/:tag", get(render_blog_post_list))
|
||||||
.route("/blog/{post_id}", get(render_blog_post))
|
.route("/blog/:post_id", get(render_blog_post))
|
||||||
.route("/broadcasts", get(render_broadcast_post_list))
|
.route("/broadcasts", get(render_broadcast_post_list))
|
||||||
.route("/broadcasts/tags/{tag}", get(render_broadcast_post_list))
|
.route("/broadcasts/tags/:tag", get(render_broadcast_post_list))
|
||||||
.route("/broadcasts/{post_id}", get(render_blog_post))
|
.route("/broadcasts/:post_id", get(render_blog_post))
|
||||||
.route("/contact", get(render_contact))
|
.route("/contact", get(render_contact))
|
||||||
.route("/showcase", get(render_projects_list))
|
.route("/showcase", get(render_projects_list))
|
||||||
.route("/showcase/m-logo-svg", get(render_animated_logo))
|
.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("/portfolio", get(render_portfolio))
|
||||||
.route("/admin", get(render_admin))
|
.route("/admin", get(render_admin))
|
||||||
.route("/feed.xml", get(render_rss_feed))
|
.route("/feed.xml", get(render_rss_feed))
|
||||||
|
BIN
static/images/uploads/dscf4274.jpg
(Stored with Git LFS)
BIN
static/images/uploads/dscf4274.jpg
(Stored with Git LFS)
Binary file not shown.
BIN
static/images/uploads/dscf4333.jpg
(Stored with Git LFS)
BIN
static/images/uploads/dscf4333.jpg
(Stored with Git LFS)
Binary file not shown.
BIN
static/images/uploads/dscf4379.jpg
(Stored with Git LFS)
BIN
static/images/uploads/dscf4379.jpg
(Stored with Git LFS)
Binary file not shown.
BIN
static/images/uploads/dscf4399.jpg
(Stored with Git LFS)
BIN
static/images/uploads/dscf4399.jpg
(Stored with Git LFS)
Binary file not shown.
BIN
static/images/uploads/img_20250607_202440.jpg
(Stored with Git LFS)
BIN
static/images/uploads/img_20250607_202440.jpg
(Stored with Git LFS)
Binary file not shown.
BIN
static/images/uploads/img_20250610_204627.jpg
(Stored with Git LFS)
BIN
static/images/uploads/img_20250610_204627.jpg
(Stored with Git LFS)
Binary file not shown.
162
styles/input.css
162
styles/input.css
@ -1,113 +1,63 @@
|
|||||||
@import "tailwindcss";
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
@theme {
|
@tailwind utilities;
|
||||||
--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 {
|
@layer base {
|
||||||
a {
|
@font-face {
|
||||||
@apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
@font-face {
|
||||||
@apply font-medium;
|
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
@apply font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-body {
|
.article-body {
|
||||||
h1 {
|
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;
|
@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 {
|
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;
|
@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;
|
||||||
}
|
}
|
||||||
@ -132,16 +82,15 @@
|
|||||||
@apply p-4;
|
@apply p-4;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@apply rounded-sm shadow-md mx-auto lg:max-w-image;
|
@apply rounded shadow-md mx-auto lg:max-w-image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
figcaption {
|
figcaption {
|
||||||
@apply mt-2 text-center text-sm italic text-blue-800 md:text-base lg:text-lg;
|
@apply mt-2 text-center text-sm italic text-blue-800 md:text-base lg:text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
@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;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
@ -170,11 +119,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:not(pre) code {
|
:not(pre) code {
|
||||||
@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;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code pre {
|
pre code pre {
|
||||||
@apply mx-2 rounded-sm lg:mx-auto lg:text-lg shadow-xs lg:max-w-note;
|
@apply mx-2 rounded lg:mx-auto lg:text-lg shadow-sm lg:max-w-note;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
@ -189,13 +138,12 @@
|
|||||||
ul {
|
ul {
|
||||||
@apply list-disc;
|
@apply list-disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
@apply list-decimal;
|
@apply list-decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
@apply rounded-sm shadow-md mx-auto lg:max-w-image;
|
@apply rounded shadow-md mx-auto lg:max-w-image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,4 +280,4 @@ article a {
|
|||||||
/* animation-duration: 5.5s; */
|
/* animation-duration: 5.5s; */
|
||||||
/* transition: transform 5.4s ease-in-out; */
|
/* transition: transform 5.4s ease-in-out; */
|
||||||
/* opacity: 1; */
|
/* opacity: 1; */
|
||||||
/* } */
|
/* } */
|
||||||
|
3806
styles/output.css
3806
styles/output.css
File diff suppressed because it is too large
Load Diff
112
tailwind.config.js
Normal file
112
tailwind.config.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/** @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">
|
<section id="recommended-articles">
|
||||||
<hr class="border-slate-300 m-5 md:my-8">
|
<hr class="border-slate-300 m-5 md:my-8">
|
||||||
|
|
||||||
<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>
|
<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>
|
||||||
<ul class="mx-5 xl:flex xl:justify-start xl:gap-10">
|
<ul class="mx-5 xl:flex xl:justify-start xl:gap-10">
|
||||||
{% for post in recommended_posts %}
|
{% for post in recommended_posts %}
|
||||||
<li class="flex-1">
|
<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>
|
<a rel="prefetch" href="/{{segment}}/{{post.slug}}" class="text-blue-950 visited:text-purple-700 no-underline">{{post.metadata.title}}</a>
|
||||||
</h3>
|
</h3>
|
||||||
</header>
|
</header>
|
||||||
<section class="text-base leading-7 text-slate-800 md:text-xl text-justify">{{post.body|truncate_md(2)|parse_markdown|safe}}</section>
|
<section class="text-base leading-5 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">
|
<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}}">
|
<ul class="inline-block" style="view-transition-name: post_tags_{{post.slug}}">
|
||||||
{% for tag in post.metadata.tags %}
|
{% for tag in post.metadata.tags %}
|
||||||
|
18
templates/components/education_card.html
Normal file
18
templates/components/education_card.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<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 border-slate-200 rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};">
|
<section class="border rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};">
|
||||||
<header class="px-4 mb-3">
|
<header class="px-4 mb-3">
|
||||||
<h2 class="text-xl font-semibold text-blue-900 md:text-2xl">
|
<h2 class="text-xl font-semibold text-blue-900 md:text-2xl">
|
||||||
{% match project.metadata.link %}
|
{% match project.metadata.link %}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<section class="flex border border-slate-200 rounded bg-white p-3">
|
<section class="flex border rounded bg-white p-3">
|
||||||
<aside class="flex justify-center items-center pr-3 shrink-0">
|
<aside class="flex justify-center items-center pr-3 shrink-0">
|
||||||
{% match skill.thumbnail %}
|
{% match skill.thumbnail %}
|
||||||
{% when Some with (source) %}
|
{% when Some with (source) %}
|
||||||
{% 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()) %}
|
{% 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()) %}
|
||||||
<figure class="mx-4 my-2">
|
<figure class="mx-4 my-2">
|
||||||
{{picture|safe}}
|
{{picture|safe}}
|
||||||
</figure>
|
</figure>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% macro social_card_start(svg, url, heading, img, class) %}
|
{% macro social_card_start(svg, url, heading, img, 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}}">
|
<a href="{{url}}" class="block no-underline border 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">
|
<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">
|
<svg role="img" aria-label="{{svg}} icon" aria-hidden="true" class="h-7 w-7 fill-blue-950">
|
||||||
<use href="#{{svg}}" />
|
<use href="#{{svg}}" />
|
||||||
@ -9,6 +9,6 @@
|
|||||||
</header>
|
</header>
|
||||||
{% let alt_text = format!("{svg} thumbnail") %}
|
{% 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-xs")).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-sm")).unwrap_or("thumbnail not found".to_string())|safe }}
|
||||||
</a>
|
</a>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% macro talent_card(svg, heading, description) %}
|
{% macro talent_card(svg, heading, description) %}
|
||||||
<section class="flex border border-slate-200 rounded-sm bg-white p-3">
|
<section class="flex border rounded bg-white p-3">
|
||||||
<aside class="flex justify-center items-center pr-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">
|
<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}}" />
|
<use href="#{{svg}}" />
|
||||||
|
18
templates/components/workplace_card.html
Normal file
18
templates/components/workplace_card.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<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">
|
<ul class="m-6 flex gap-2 flex-wrap justify-center">
|
||||||
{% for technology in technology_list %}
|
{% for technology in technology_list %}
|
||||||
<li class="p-2 text-pink-900 bg-blue-100 text-sm border rounded-sm border-blue-300 font-mono">
|
<li class="p-2 text-pink-900 bg-blue-100 text-sm border rounded border-blue-300 font-mono">
|
||||||
{{technology}}
|
{{technology}}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% when None %}
|
{% when None %}
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
<aside class="flex logo-section grow justify-end content-end">
|
<aside class="flex logo-section flex-grow justify-end content-end">
|
||||||
<a class="logo p-3 text-base" href="/">
|
<a class="logo p-3 text-base" href="/">
|
||||||
@michalvankodev
|
@michalvankodev
|
||||||
</a>
|
</a>
|
||||||
|
Reference in New Issue
Block a user