Compare commits
7 Commits
main
...
misc/migra
Author | SHA1 | Date | |
---|---|---|---|
6a5a9c890f | |||
5ba193cd56 | |||
95b105762d | |||
38e26ebf1f | |||
fa9b104f60 | |||
b47e8e18d0 | |||
546bf4400d |
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,3 +23,6 @@ Cargo.lock
|
|||||||
|
|
||||||
# Image generator
|
# Image generator
|
||||||
generated_images/
|
generated_images/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
@ -1,29 +1,26 @@
|
|||||||
layout {
|
layout {
|
||||||
pane split_direction="vertical" focus=true {
|
default_tab_template {
|
||||||
pane edit="src/main.rs"
|
children
|
||||||
pane split_direction="horizontal" size=60 {
|
pane size=2 borderless=true {
|
||||||
just { args "server_dev"; }
|
plugin location="zellij:status-bar"
|
||||||
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 {
|
|
||||||
pane {
|
tab split_direction="vertical" focus=true {
|
||||||
command "just"
|
pane {
|
||||||
args "tailwind"
|
just { args "server_dev"; }
|
||||||
|
just { args "test"; }
|
||||||
}
|
}
|
||||||
pane {
|
pane {
|
||||||
command "just"
|
just { args "tailwind"; }
|
||||||
args "decap_server"
|
just { args "decap_server"; }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
pane size=2 borderless=true {
|
|
||||||
plugin location="zellij:status-bar"
|
|
||||||
}
|
|
||||||
pane size=1 borderless=true {
|
|
||||||
plugin location="zellij:tab-bar"
|
|
||||||
}
|
}
|
||||||
|
tab
|
||||||
}
|
}
|
||||||
|
24
Cargo.toml
24
Cargo.toml
@ -6,8 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
askama = { version = "0.14", features = ["with-axum", "mime", "mime_guess"] }
|
askama = { version = "0.14" }
|
||||||
askama_axum = "0.5.0"
|
|
||||||
axum = "0.8.0"
|
axum = "0.8.0"
|
||||||
chrono = { version = "0.4.31", features = ["serde"] }
|
chrono = { version = "0.4.31", features = ["serde"] }
|
||||||
pulldown-cmark = { version = "0.13" }
|
pulldown-cmark = { version = "0.13" }
|
||||||
@ -28,22 +27,5 @@ indoc = "2.0.5"
|
|||||||
askama_escape = "0.13.0"
|
askama_escape = "0.13.0"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
|
|
||||||
[build]
|
[dev-dependencies]
|
||||||
rustflags = ["-Z", "threads=8"]
|
pretty_assertions = "1"
|
||||||
|
|
||||||
# [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
|
|
||||||
|
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 -i ./styles/input.css -o ./styles/output.css --watch
|
npx @tailwindcss/cli -i ./styles/input.css -o ./styles/output.css --watch
|
||||||
|
|
||||||
# svg sprite creation
|
# svg sprite creation
|
||||||
svgstore:
|
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::http::{header, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use chrono::Utc;
|
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::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
|
||||||
@ -27,14 +36,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 =
|
let truncated = truncate_md(&post.body, &EmptyValues, 2)
|
||||||
truncate_md(&post.body, 2).unwrap_or("Can't parse post body".to_string());
|
.unwrap_or("Can't parse post body".to_string());
|
||||||
let parsed_md = parse_markdown(&truncated)
|
let parsed_md = parse_markdown(&truncated, &EmptyValues)
|
||||||
.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)
|
let parsed_md = parse_markdown(&post.body, &EmptyValues)
|
||||||
.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,3 +1,4 @@
|
|||||||
|
use core::fmt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use image::image_dimensions;
|
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(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();
|
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);
|
||||||
@ -34,7 +38,8 @@ pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> {
|
|||||||
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 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
|
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>) -> ::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"));
|
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, rows: usize) -> ::askama::Result<String> {
|
pub fn truncate_md(body: &str, _: &dyn askama::Values, rows: usize) -> ::askama::Result<String> {
|
||||||
let description = body
|
let description = body
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|line| {
|
.filter(|line| {
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
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() -> AdminPageTemplate {
|
pub async fn render_admin() -> Result<impl IntoResponse, StatusCode> {
|
||||||
AdminPageTemplate {}
|
Ok(Html(
|
||||||
|
AdminPageTemplate {}
|
||||||
|
.render()
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::http::StatusCode;
|
use axum::{
|
||||||
|
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<AnimatedLogoTemplate, StatusCode> {
|
pub async fn render_animated_logo() -> Result<impl IntoResponse, StatusCode> {
|
||||||
Ok(AnimatedLogoTemplate {})
|
Ok(Html(AnimatedLogoTemplate {}.render().unwrap()))
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
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;
|
||||||
@ -22,7 +24,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<PostListTemplate, StatusCode> {
|
) -> Result<impl IntoResponse, 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);
|
||||||
|
|
||||||
@ -50,14 +52,18 @@ pub async fn render_blog_post_list(
|
|||||||
("Blog posts".to_string(), "Blog posts".to_string())
|
("Blog posts".to_string(), "Blog posts".to_string())
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PostListTemplate {
|
Ok(Html(
|
||||||
title,
|
PostListTemplate {
|
||||||
og_title,
|
title,
|
||||||
segment: Segment::Blog,
|
og_title,
|
||||||
posts,
|
segment: Segment::Blog,
|
||||||
header_props,
|
posts,
|
||||||
tags: blog_tags,
|
header_props,
|
||||||
featured_projects,
|
tags: blog_tags,
|
||||||
current_url: original_uri.to_string(),
|
featured_projects,
|
||||||
})
|
current_url: original_uri.to_string(),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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};
|
||||||
|
|
||||||
@ -30,7 +31,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<BlogPostTemplate, StatusCode> {
|
) -> Result<impl IntoResponse, 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") {
|
||||||
@ -59,17 +60,21 @@ pub async fn render_blog_post(
|
|||||||
_ => HeaderProps::default(),
|
_ => HeaderProps::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(BlogPostTemplate {
|
Ok(Html(
|
||||||
title: post.metadata.title,
|
BlogPostTemplate {
|
||||||
date: post.metadata.date,
|
title: post.metadata.title,
|
||||||
tags: post.metadata.tags,
|
date: post.metadata.date,
|
||||||
body: post.body,
|
tags: post.metadata.tags,
|
||||||
slug: post.slug,
|
body: post.body,
|
||||||
segment,
|
slug: post.slug,
|
||||||
thumbnail: post.metadata.thumbnail,
|
segment,
|
||||||
header_props,
|
thumbnail: post.metadata.thumbnail,
|
||||||
recommended_posts,
|
header_props,
|
||||||
})
|
recommended_posts,
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_recommended_posts(
|
async fn get_recommended_posts(
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
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;
|
||||||
@ -21,7 +23,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<PostListTemplate, StatusCode> {
|
) -> Result<impl IntoResponse, 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);
|
||||||
|
|
||||||
@ -50,14 +52,18 @@ pub async fn render_broadcast_post_list(
|
|||||||
"Broadcasts".to_string()
|
"Broadcasts".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(PostListTemplate {
|
Ok(Html(
|
||||||
title: title.clone(),
|
PostListTemplate {
|
||||||
og_title: title,
|
title: title.clone(),
|
||||||
segment: Segment::Broadcasts,
|
og_title: title,
|
||||||
posts,
|
segment: Segment::Broadcasts,
|
||||||
header_props,
|
posts,
|
||||||
tags: popular_tags,
|
header_props,
|
||||||
featured_projects,
|
tags: popular_tags,
|
||||||
current_url: original_uri.to_string(),
|
featured_projects,
|
||||||
})
|
current_url: original_uri.to_string(),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::http::StatusCode;
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{Html, IntoResponse},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::components::site_header::HeaderProps;
|
use crate::components::site_header::HeaderProps;
|
||||||
|
|
||||||
@ -18,7 +21,7 @@ pub struct ContactPageTemplate {
|
|||||||
pub links: Vec<ContactLink>,
|
pub links: Vec<ContactLink>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
|
pub async fn render_contact() -> Result<impl IntoResponse, StatusCode> {
|
||||||
let links = vec![
|
let links = vec![
|
||||||
ContactLink {
|
ContactLink {
|
||||||
href: "mailto:michalvankosk@gmail.com".to_string(),
|
href: "mailto:michalvankosk@gmail.com".to_string(),
|
||||||
@ -76,9 +79,13 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
Ok(ContactPageTemplate {
|
Ok(Html(
|
||||||
title: "Contact".to_owned(),
|
ContactPageTemplate {
|
||||||
header_props: HeaderProps::default(),
|
title: "Contact".to_owned(),
|
||||||
links,
|
header_props: HeaderProps::default(),
|
||||||
})
|
links,
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::http::StatusCode;
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{Html, IntoResponse},
|
||||||
|
};
|
||||||
use tokio::try_join;
|
use tokio::try_join;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -26,7 +29,7 @@ pub struct IndexTemplate {
|
|||||||
featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>,
|
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!(
|
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)),
|
||||||
@ -44,12 +47,16 @@ pub async fn render_index() -> Result<IndexTemplate, 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(IndexTemplate {
|
Ok(Html(
|
||||||
header_props: HeaderProps::default(),
|
IndexTemplate {
|
||||||
blog_tags,
|
header_props: HeaderProps::default(),
|
||||||
broadcasts_tags,
|
blog_tags,
|
||||||
featured_blog_posts,
|
broadcasts_tags,
|
||||||
featured_projects,
|
featured_blog_posts,
|
||||||
featured_broadcasts,
|
featured_projects,
|
||||||
})
|
featured_broadcasts,
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{extract::OriginalUri, http::StatusCode};
|
use axum::{
|
||||||
|
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;
|
||||||
@ -13,13 +17,17 @@ 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, NotFoundPage), StatusCode> {
|
) -> Result<(StatusCode, impl IntoResponse), StatusCode> {
|
||||||
info!("{original_uri} not found");
|
info!("{original_uri} not found");
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
NotFoundPage {
|
Html(
|
||||||
title: "This page does not exists".to_owned(),
|
NotFoundPage {
|
||||||
header_props: HeaderProps::default(),
|
title: "This page does not exists".to_owned(),
|
||||||
},
|
header_props: HeaderProps::default(),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::http::StatusCode;
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{Html, IntoResponse},
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -50,7 +53,7 @@ pub struct PortfolioTemplate {
|
|||||||
pub technology_list: Vec<String>,
|
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 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?;
|
||||||
@ -126,14 +129,18 @@ pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
|
|||||||
.map(|str| str.to_owned())
|
.map(|str| str.to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(PortfolioTemplate {
|
Ok(Html(
|
||||||
title: "Portfolio".to_owned(),
|
PortfolioTemplate {
|
||||||
body: portfolio.body,
|
title: "Portfolio".to_owned(),
|
||||||
header_props: HeaderProps::default(),
|
body: portfolio.body,
|
||||||
project_list,
|
header_props: HeaderProps::default(),
|
||||||
workplace_list,
|
project_list,
|
||||||
education_list,
|
workplace_list,
|
||||||
contact_links,
|
education_list,
|
||||||
technology_list,
|
contact_links,
|
||||||
})
|
technology_list,
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::http::StatusCode;
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{Html, IntoResponse},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::site_header::HeaderProps,
|
components::site_header::HeaderProps,
|
||||||
@ -16,16 +19,20 @@ pub struct ProjectListTemplate {
|
|||||||
pub header_props: HeaderProps,
|
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?;
|
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(ProjectListTemplate {
|
Ok(Html(
|
||||||
title: "Showcase".to_owned(),
|
ProjectListTemplate {
|
||||||
header_props: HeaderProps::default(),
|
title: "Showcase".to_owned(),
|
||||||
project_list,
|
header_props: HeaderProps::default(),
|
||||||
})
|
project_list,
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::http::StatusCode;
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{Html, IntoResponse},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::components::site_header::HeaderProps;
|
use crate::components::site_header::HeaderProps;
|
||||||
|
|
||||||
@ -9,8 +12,12 @@ pub struct EggFetcherShowcaseTemplate {
|
|||||||
header_props: HeaderProps,
|
header_props: HeaderProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_egg_fetcher() -> Result<EggFetcherShowcaseTemplate, StatusCode> {
|
pub async fn render_egg_fetcher() -> Result<impl IntoResponse, StatusCode> {
|
||||||
Ok(EggFetcherShowcaseTemplate {
|
Ok(Html(
|
||||||
header_props: HeaderProps::default(),
|
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() {
|
fn test_get_export_formats() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_export_formats("/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,24 +293,21 @@ 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.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"
|
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/avif"
|
type="image/jpeg"
|
||||||
>
|
>
|
||||||
<source
|
<img
|
||||||
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"
|
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
|
||||||
type="image/jpeg"
|
width="300"
|
||||||
>
|
height="200"
|
||||||
<img
|
alt="Testing image alt"
|
||||||
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
|
|
||||||
width="300"
|
>
|
||||||
height="200"
|
</picture>"#,
|
||||||
alt="Testing image alt"
|
|
||||||
>
|
|
||||||
</picture>"#,
|
|
||||||
};
|
};
|
||||||
assert_eq!(
|
pretty_assertions::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,7 +8,14 @@ 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 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 tower_http::trace::TraceLayer;
|
||||||
use tracing::info_span;
|
use tracing::info_span;
|
||||||
|
|
||||||
@ -16,15 +23,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))
|
||||||
|
162
styles/input.css
162
styles/input.css
@ -1,63 +1,113 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
@theme {
|
||||||
|
--font-sans: "Baloo2", "Baloo2 Noto Fallback", "Baloo2 Fallback", "ui-sans-serif", "system-ui", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
|
||||||
|
--text-readxl: 1.75rem;
|
||||||
|
--text-readxl--line-height: 2.25rem;
|
||||||
|
--text-readxl--letter-spacing: -0.015rem;
|
||||||
|
--text-readxl--font-weight: 400;
|
||||||
|
|
||||||
|
--spacing-note: 60rem;
|
||||||
|
--spacing-read: 64rem;
|
||||||
|
--spacing-image: min(70rem, 95vw);
|
||||||
|
--spacing-maxindex: 100rem;
|
||||||
|
|
||||||
|
--color-blue-50: #f1f7fe;
|
||||||
|
--color-blue-100: #e1effd;
|
||||||
|
--color-blue-200: #bddefa;
|
||||||
|
--color-blue-300: #82c3f7;
|
||||||
|
--color-blue-400: #42a6f0;
|
||||||
|
--color-blue-500: #1789e0;
|
||||||
|
--color-blue-600: #0a6cbf;
|
||||||
|
--color-blue-700: #0a569a;
|
||||||
|
--color-blue-800: #0c4980;
|
||||||
|
--color-blue-900: #103e6a;
|
||||||
|
--color-blue-950: #0b2746;
|
||||||
|
|
||||||
|
--color-pink-50: #fff4fd;
|
||||||
|
--color-pink-100: #ffe7fb;
|
||||||
|
--color-pink-200: #ffcff7;
|
||||||
|
--color-pink-300: #fea6eb;
|
||||||
|
--color-pink-400: #fc76dd;
|
||||||
|
--color-pink-500: #f342ca;
|
||||||
|
--color-pink-600: #d722a9;
|
||||||
|
--color-pink-700: #b31889;
|
||||||
|
--color-pink-800: #92166e;
|
||||||
|
--color-pink-900: #771859;
|
||||||
|
--color-pink-950: #500238;
|
||||||
|
|
||||||
|
--color-purple-50: #F8F5FC;
|
||||||
|
--color-purple-100: #D5C2ED;
|
||||||
|
--color-purple-200: #B28EDE;
|
||||||
|
--color-purple-300: #8F5BCF;
|
||||||
|
--color-purple-400: #6D30B9;
|
||||||
|
--color-purple-500: #5F2AA2;
|
||||||
|
--color-purple-600: #52248A;
|
||||||
|
--color-purple-700: #441E73;
|
||||||
|
--color-purple-800: #36185C;
|
||||||
|
--color-purple-900: #281244;
|
||||||
|
--color-purple-950: #1A0C2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Baloo2';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
src:
|
||||||
|
local('Baloo2'),
|
||||||
|
url(/fonts/baloo2/Baloo2-Latin-Variable-wght.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Baloo 2';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/baloo2/Baloo2-Latin-Variable-ext-wght.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||||
|
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Baloo2 Fallback';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'),
|
||||||
|
local('Arial');
|
||||||
|
ascent-override: 111.2%;
|
||||||
|
descent-override: 54.05%;
|
||||||
|
line-gap-override: 0%;
|
||||||
|
size-adjust: 96.95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Baloo2 Noto Fallback';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Noto Sans');
|
||||||
|
ascent-override: 88%;
|
||||||
|
descent-override: none;
|
||||||
|
line-gap-override: 0%;
|
||||||
|
size-adjust: 92%;
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@font-face {
|
a {
|
||||||
font-family: 'Baloo2';
|
@apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500;
|
||||||
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 {
|
strong {
|
||||||
font-family: 'Baloo2 Fallback';
|
@apply font-medium;
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -82,15 +132,16 @@ strong {
|
|||||||
@apply p-4;
|
@apply p-4;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@apply rounded shadow-md mx-auto lg:max-w-image;
|
@apply rounded-sm 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 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 {
|
thead {
|
||||||
@ -119,11 +170,11 @@ strong {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:not(pre) code {
|
: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 {
|
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,
|
ul,
|
||||||
@ -138,12 +189,13 @@ strong {
|
|||||||
ul {
|
ul {
|
||||||
@apply list-disc;
|
@apply list-disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
@apply list-decimal;
|
@apply list-decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
@apply rounded shadow-md mx-auto lg:max-w-image;
|
@apply rounded-sm shadow-md mx-auto lg:max-w-image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,4 +332,4 @@ article a {
|
|||||||
/* animation-duration: 5.5s; */
|
/* animation-duration: 5.5s; */
|
||||||
/* transition: transform 5.4s ease-in-out; */
|
/* transition: transform 5.4s ease-in-out; */
|
||||||
/* opacity: 1; */
|
/* opacity: 1; */
|
||||||
/* } */
|
/* } */
|
3814
styles/output.css
3814
styles/output.css
File diff suppressed because it is too large
Load Diff
@ -1,112 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: ["./templates/**/**.html"],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
sans: [
|
|
||||||
"Baloo2",
|
|
||||||
"Baloo2 Noto Fallback",
|
|
||||||
"Baloo2 Fallback",
|
|
||||||
"ui-sans-serif",
|
|
||||||
"system-ui",
|
|
||||||
"sans-serif",
|
|
||||||
"Apple Color Emoji",
|
|
||||||
"Segoe UI Emoji",
|
|
||||||
"Segoe UI Symbol",
|
|
||||||
"Noto Color Emoji",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
spacing: {
|
|
||||||
note: "60rem",
|
|
||||||
read: "64rem",
|
|
||||||
image: "min(70rem, 95vw)",
|
|
||||||
maxindex: "100rem",
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
note: "60rem",
|
|
||||||
read: "64rem",
|
|
||||||
image: "min(70rem, 95vw)",
|
|
||||||
maxindex: "100rem",
|
|
||||||
},
|
|
||||||
fontSize: {
|
|
||||||
readxl: [
|
|
||||||
"1.75rem",
|
|
||||||
{
|
|
||||||
lineHeight: "2.25rem",
|
|
||||||
letterSpacing: "-0.015em",
|
|
||||||
fontWeight: "400",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
// blue: {
|
|
||||||
// 50: "#ecf6fe",
|
|
||||||
// 100: "#d9edfc",
|
|
||||||
// 200: "#b3dbf9",
|
|
||||||
// 300: "#8ecaf6",
|
|
||||||
// 400: "#68b8f3",
|
|
||||||
// 500: "#42a6f0",
|
|
||||||
// 600: "#3585c0",
|
|
||||||
// 700: "#286490",
|
|
||||||
// 800: "#1F4E71",
|
|
||||||
// 900: "#173A54",
|
|
||||||
// 950: "#0F2637",
|
|
||||||
// },
|
|
||||||
blue: {
|
|
||||||
50: "#f1f7fe",
|
|
||||||
100: "#e1effd",
|
|
||||||
200: "#bddefa",
|
|
||||||
300: "#82c3f7",
|
|
||||||
400: "#42a6f0",
|
|
||||||
500: "#1789e0",
|
|
||||||
600: "#0a6cbf",
|
|
||||||
700: "#0a569a",
|
|
||||||
800: "#0c4980",
|
|
||||||
900: "#103e6a",
|
|
||||||
950: "#0b2746",
|
|
||||||
},
|
|
||||||
// pink: {
|
|
||||||
// 50: "#FFFBFE",
|
|
||||||
// 100: "#FFE4F9",
|
|
||||||
// 200: "#FECEF4",
|
|
||||||
// 300: "#FEB8EF",
|
|
||||||
// 400: "#fea6eb",
|
|
||||||
// 500: "#D38AC3",
|
|
||||||
// 600: "#B476A7",
|
|
||||||
// 700: "#96628B",
|
|
||||||
// 800: "#774E6E",
|
|
||||||
// 900: "#593A52",
|
|
||||||
// 950: "#3A2636",
|
|
||||||
// },
|
|
||||||
pink: {
|
|
||||||
50: "#fff4fd",
|
|
||||||
100: "#ffe7fb",
|
|
||||||
200: "#ffcff7",
|
|
||||||
300: "#fea6eb",
|
|
||||||
400: "#fc76dd",
|
|
||||||
500: "#f342ca",
|
|
||||||
600: "#d722a9",
|
|
||||||
700: "#b31889",
|
|
||||||
800: "#92166e",
|
|
||||||
900: "#771859",
|
|
||||||
950: "#500238",
|
|
||||||
},
|
|
||||||
purple: {
|
|
||||||
50: "#F8F5FC",
|
|
||||||
100: "#D5C2ED",
|
|
||||||
200: "#B28EDE",
|
|
||||||
300: "#8F5BCF",
|
|
||||||
400: "#6D30B9",
|
|
||||||
500: "#5F2AA2",
|
|
||||||
600: "#52248A",
|
|
||||||
700: "#441E73",
|
|
||||||
800: "#36185C",
|
|
||||||
900: "#281244",
|
|
||||||
950: "#1A0C2D",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
@ -41,7 +41,7 @@
|
|||||||
<section id="recommended-articles">
|
<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 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">
|
<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-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">
|
<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 %}
|
||||||
|
@ -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">
|
<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,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">
|
<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 skill_name = skill.name.clone() %}
|
{% 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 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 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">
|
<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-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>
|
</a>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{% macro talent_card(svg, heading, description) %}
|
{% 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">
|
<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}}" />
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
<section class="flex border rounded bg-white p-3">
|
|
||||||
<aside class="flex justify-center items-center pr-3 shrink-0">
|
|
||||||
{% match workplace.thumbnail %}
|
|
||||||
{% when Some with (source) %}
|
|
||||||
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "Workplace cover", Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
|
|
||||||
<figure class="mx-4 my-2">
|
|
||||||
{{picture|safe}}
|
|
||||||
</figure>
|
|
||||||
{% when None %}
|
|
||||||
{% endmatch %}
|
|
||||||
</aside>
|
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<h3 class="text-lg font-medium mb-1 md:text-2xl">{{workplace.name}}</h3>
|
|
||||||
</header>
|
|
||||||
<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{workplace.description|parse_markdown|safe}}</section>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
@ -174,7 +174,7 @@
|
|||||||
|
|
||||||
<ul class="m-6 flex gap-2 flex-wrap justify-center">
|
<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 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}}
|
{{technology}}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% when None %}
|
{% when None %}
|
||||||
{% endmatch %}
|
{% 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="/">
|
<a class="logo p-3 text-base" href="/">
|
||||||
@michalvankodev
|
@michalvankodev
|
||||||
</a>
|
</a>
|
||||||
|
Reference in New Issue
Block a user