diff --git a/_posts/blog/2020-05-11-transition-to-colemak-layout.md b/_posts/blog/2020-05-11-transition-to-colemak-layout.md index 13fec46..d41881c 100644 --- a/_posts/blog/2020-05-11-transition-to-colemak-layout.md +++ b/_posts/blog/2020-05-11-transition-to-colemak-layout.md @@ -3,6 +3,7 @@ layout: blog title: Transition to Colemak keyboard layout segments: - blog + - featured published: true date: 2020-05-11T05:38:18.797Z tags: diff --git a/_posts/blog/2022-02-28-error-handling-with-either-type.md b/_posts/blog/2022-02-28-error-handling-with-either-type.md index 91a0f5e..831f782 100644 --- a/_posts/blog/2022-02-28-error-handling-with-either-type.md +++ b/_posts/blog/2022-02-28-error-handling-with-either-type.md @@ -3,13 +3,13 @@ layout: blog title: Error handling with Either segments: - blog + - featured published: true date: 2022-02-28T11:30:54.195Z tags: - Development - Guide --- - We have started a new small internal project for automating a few workflows around counting worked hours and time offs. ## Application architecture @@ -22,7 +22,7 @@ It will run on a server with a possibility of migrating into serverless when we As it is not a classic web server application I had to come up with slightly different error handling as we are used to. I've been trying to find a semi-functional API with all the good practices described in my [guide on error handling](/blog/2020-12-09-guide-on-error-handling). The main goal is to not let users be presented with internal information about errors. We want to show user-friendly messages instead. I call this API semi-functional as **I didn't want to use monads** and go 100% functional. We use simple asynchronous functions to handle interactions. -The goal is to handle errors that are expected. Unexpected errors should still be thrown and caught by an _"Error boundary"_ around the whole app that will handle and log the error. +The goal is to handle errors that are expected. Unexpected errors should still be thrown and caught by an *"Error boundary"* around the whole app that will handle and log the error. ## Error types diff --git a/_posts/blog/2022-06-26-our-attempt-at-rusty-game-jam-weekly-25-2022.md b/_posts/blog/2022-06-26-our-attempt-at-rusty-game-jam-weekly-25-2022.md index 759a7cd..c49efc0 100644 --- a/_posts/blog/2022-06-26-our-attempt-at-rusty-game-jam-weekly-25-2022.md +++ b/_posts/blog/2022-06-26-our-attempt-at-rusty-game-jam-weekly-25-2022.md @@ -3,6 +3,7 @@ layout: blog title: "Our attempt at Rusty game jam - Weekly #25-2022" segments: - blog + - featured published: true date: 2022-06-26T20:02:47.419Z tags: diff --git a/_posts/blog/2023-04-27-keyboards-ergonomics-of-21st-century.md b/_posts/blog/2023-04-27-keyboards-ergonomics-of-21st-century.md index 9a408a2..7e96bfa 100644 --- a/_posts/blog/2023-04-27-keyboards-ergonomics-of-21st-century.md +++ b/_posts/blog/2023-04-27-keyboards-ergonomics-of-21st-century.md @@ -1,8 +1,9 @@ --- layout: blog -title: "Keyboards & ergonomics of 21st century" +title: Keyboards & ergonomics of 21st century segments: - broadcasts + - featured published: true date: 2023-04-27T21:22:21.191Z tags: diff --git a/_posts/blog/2023-08-29-i-built-my-3rd-custom-keyboard.md b/_posts/blog/2023-08-29-i-built-my-3rd-custom-keyboard.md index e74ee27..79f75e0 100644 --- a/_posts/blog/2023-08-29-i-built-my-3rd-custom-keyboard.md +++ b/_posts/blog/2023-08-29-i-built-my-3rd-custom-keyboard.md @@ -3,6 +3,7 @@ layout: blog title: I built my 3rd custom keyboard segments: - blog + - featured published: true date: 2023-08-29T19:34:17.071Z tags: diff --git a/axum_server/justfile b/axum_server/justfile index 664bea4..124f5cf 100644 --- a/axum_server/justfile +++ b/axum_server/justfile @@ -13,6 +13,11 @@ svgstore: server_dev: cargo watch -x run +# CMS server for local dev +# TODO #directory-swap +decap_server: + cd .. && npx decap-server + # Run dev server in watch mode dev: (just server_dev; just tailwind) | parallel diff --git a/axum_server/src/featured_posts.rs b/axum_server/src/featured_posts.rs new file mode 100644 index 0000000..c4415c1 --- /dev/null +++ b/axum_server/src/featured_posts.rs @@ -0,0 +1,13 @@ +use crate::{pages::post::PostMetadata, post_list::get_post_list, post_parser::ParseResult}; +use axum::http::StatusCode; + +pub async fn get_featured_posts() -> Result>, StatusCode> { + let post_list = get_post_list::().await?; + + let featured_posts = post_list + .into_iter() + .filter(|post| post.metadata.segments.contains(&"featured".to_string())) + .collect(); + + Ok(featured_posts) +} diff --git a/axum_server/src/filters.rs b/axum_server/src/filters.rs index 73a16b6..5224d7f 100644 --- a/axum_server/src/filters.rs +++ b/axum_server/src/filters.rs @@ -1,7 +1,20 @@ use chrono::{DateTime, Utc}; +use tracing::debug; // This filter does not have extra arguments pub fn pretty_date(date_time: &DateTime) -> ::askama::Result { let formatted = format!("{}", date_time.format("%e %B %Y")); Ok(formatted) } + +// This filter does not have extra arguments +pub fn description_filter(body: &String) -> ::askama::Result { + let description = body + .lines() + .filter(|line| line.starts_with(">() + .join("\n"); + debug!(description); + Ok(description) +} diff --git a/axum_server/src/main.rs b/axum_server/src/main.rs index 2699dd0..761a315 100644 --- a/axum_server/src/main.rs +++ b/axum_server/src/main.rs @@ -1,9 +1,10 @@ -use axum; +use axum::{self}; use tower_http::services::ServeDir; use tower_livereload::LiveReloadLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod components; +mod featured_posts; mod feed; mod filters; mod pages; @@ -30,7 +31,11 @@ async fn main() { let app = router::get_router() .nest_service("/styles", ServeDir::new("styles")) .nest_service("/images", ServeDir::new("../static/images")) - .nest_service("/svg", ServeDir::new("../static/svg")); + .nest_service("/svg", ServeDir::new("../static/svg")) + .nest_service( + "/config.yml", + ServeDir::new("../static/resources/config.yml"), + ); #[cfg(debug_assertions)] let app = app.layer(LiveReloadLayer::new()); diff --git a/axum_server/src/pages/admin.rs b/axum_server/src/pages/admin.rs new file mode 100644 index 0000000..fd04e61 --- /dev/null +++ b/axum_server/src/pages/admin.rs @@ -0,0 +1,9 @@ +use askama::Template; + +#[derive(Template)] +#[template(path = "admin.html")] +pub struct AdminPageTemplate {} + +pub async fn render_admin() -> AdminPageTemplate { + AdminPageTemplate {} +} diff --git a/axum_server/src/pages/index.rs b/axum_server/src/pages/index.rs index a8a1b49..220f6c1 100644 --- a/axum_server/src/pages/index.rs +++ b/axum_server/src/pages/index.rs @@ -1,25 +1,32 @@ use askama::Template; use axum::http::StatusCode; +use crate::filters; use crate::{ components::{ site_footer::{render_site_footer, SiteFooter}, site_header::HeaderProps, }, + featured_posts::get_featured_posts, + post_parser::ParseResult, tag_list::get_popular_blog_tags, }; +use super::post::PostMetadata; + #[derive(Template)] #[template(path = "index.html")] pub struct IndexTemplate { site_footer: SiteFooter, header_props: HeaderProps, blog_tags: Vec, + featured_posts: Vec>, } pub async fn render_index() -> Result { let site_footer = tokio::spawn(render_site_footer()); let blog_tags = tokio::spawn(get_popular_blog_tags()); + let featured_posts = tokio::spawn(get_featured_posts()); let blog_tags = blog_tags .await @@ -29,9 +36,14 @@ pub async fn render_index() -> Result { .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let featured_posts = featured_posts + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)??; + Ok(IndexTemplate { site_footer, header_props: HeaderProps::default(), blog_tags, + featured_posts, }) } diff --git a/axum_server/src/pages/mod.rs b/axum_server/src/pages/mod.rs index ee745d9..dc99cba 100644 --- a/axum_server/src/pages/mod.rs +++ b/axum_server/src/pages/mod.rs @@ -1,3 +1,4 @@ +pub mod admin; pub mod contact; pub mod index; pub mod post; diff --git a/axum_server/src/router.rs b/axum_server/src/router.rs index 591d702..1f87198 100644 --- a/axum_server/src/router.rs +++ b/axum_server/src/router.rs @@ -1,7 +1,7 @@ use crate::{ feed::render_rss_feed, pages::{ - contact::render_contact, index::render_index, post::render_post, + admin::render_admin, contact::render_contact, index::render_index, post::render_post, post_list::render_post_list, }, }; @@ -16,6 +16,7 @@ pub fn get_router() -> Router { .route("/blog/tags/:tag", get(render_post_list)) .route("/blog/:post_id", get(render_post)) .route("/contact", get(render_contact)) + .route("/admin", get(render_admin)) .route("/feed.xml", get(render_rss_feed)) .layer( TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { diff --git a/axum_server/styles/output.css b/axum_server/styles/output.css index 6d05424..5426b51 100644 --- a/axum_server/styles/output.css +++ b/axum_server/styles/output.css @@ -554,6 +554,10 @@ video { --tw-contain-style: ; } +.row-span-3 { + grid-row: span 3 / span 3; +} + .m-1 { margin: 0.25rem; } @@ -596,16 +600,6 @@ video { margin-right: 1.5rem; } -.my-0 { - margin-top: 0px; - margin-bottom: 0px; -} - -.my-0\.5 { - margin-top: 0.125rem; - margin-bottom: 0.125rem; -} - .my-12 { margin-top: 3rem; margin-bottom: 3rem; @@ -653,6 +647,10 @@ video { display: flex; } +.grid { + display: grid; +} + .hidden { display: none; } @@ -685,6 +683,14 @@ video { flex-grow: 1; } +.grid-flow-col { + grid-auto-flow: column; +} + +.grid-rows-3 { + grid-template-rows: repeat(3, minmax(0, 1fr)); +} + .flex-row { flex-direction: row; } @@ -713,6 +719,10 @@ video { justify-content: space-between; } +.gap-4 { + gap: 1rem; +} + .rounded { border-radius: 0.25rem; } diff --git a/axum_server/templates/admin.html b/axum_server/templates/admin.html new file mode 100644 index 0000000..c12be28 --- /dev/null +++ b/axum_server/templates/admin.html @@ -0,0 +1,15 @@ + + + + + + + + + Content Manager + + + + + + diff --git a/axum_server/templates/components/post_preview.html b/axum_server/templates/components/post_preview.html new file mode 100644 index 0000000..1d6522a --- /dev/null +++ b/axum_server/templates/components/post_preview.html @@ -0,0 +1,15 @@ +
+ +
+

{{post.metadata.title}}

+
+
{{post.body|description_filter|safe}}
+
+ Footrik +
+
+ diff --git a/axum_server/templates/index.html b/axum_server/templates/index.html index 187771a..5386630 100644 --- a/axum_server/templates/index.html +++ b/axum_server/templates/index.html @@ -51,7 +51,11 @@
    - + {% for post in featured_posts %} +
  • + {% include "components/post_preview.html" %} +
  • + {% endfor %}
diff --git a/static/admin/config.yml b/static/admin/config.yml index 446a536..f0a9718 100644 --- a/static/admin/config.yml +++ b/static/admin/config.yml @@ -24,6 +24,7 @@ collections: widget: 'select' multiple: true options: + - { label: 'OLAOLA', value: 'featured' } - { label: 'Blog', value: 'blog' } - { label: 'Broadcasts', value: 'broadcasts' } - { label: 'Cookbook', value: 'cookbook' } diff --git a/static/resources/config.yml b/static/resources/config.yml new file mode 100644 index 0000000..a146d9f --- /dev/null +++ b/static/resources/config.yml @@ -0,0 +1,141 @@ +backend: + name: git-gateway + repo: michalvankodev/michalvankodev + branch: master # Branch to update (optional; defaults to master) + site_domain: michalvanko.dev + +# when using the default proxy server port +local_backend: true + +media_folder: 'static/images/uploads' # Media files will be stored in the repo under images/uploads +public_folder: '/images/uploads' # The src attribute for uploaded media will begin with /images/uploads + +collections: + - name: 'blog' # Used in routes, e.g., /admin/collections/blog + label: 'Blog' # Used in the UI + folder: '_posts/blog' # The path to the folder where the documents are stored + create: true # Allow users to create new documents in this collection + slug: '{{year}}-{{month}}-{{day}}-{{slug}}' # Filename template, e.g., YYYY-MM-DD-title.md + fields: # The fields for each document, usually in front matter + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' } + - { label: 'Title', name: 'title', widget: 'string' } + - label: 'Segments' + name: 'segments' + widget: 'select' + multiple: true + options: + - { label: 'Featured', value: 'featured' } + - { label: 'Blog', value: 'blog' } + - { label: 'Broadcasts', value: 'broadcasts' } + - { label: 'Cookbook', value: 'cookbook' } + default: ['blog'] + - { + label: 'Published', + name: 'published', + widget: 'boolean', + default: true, + } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { + label: 'Featured Image', + name: 'thumbnail', + widget: 'image', + required: false, + } + - { + label: 'Tags', + name: 'tags', + widget: 'list', + default: ['News'], + required: false, + } + - { label: 'Body', name: 'body', widget: 'markdown' } + - { + label: 'Writers notes', + name: 'notes', + widget: 'markdown', + required: false, + } + - name: 'pages' + label: 'Pages' + files: + - label: 'Portfolio' + name: 'portfolio' + file: '_pages/portfolio.md' + fields: + - { label: Title, name: title, widget: string } + - { label: Body, name: body, widget: markdown } + - { + label: Work history prelude, + name: work_history_prelude, + widget: markdown, + } + - label: Work history + name: work_history + widget: list + fields: + - { label: Company name, name: name, widget: string } + - { label: Description, name: description, widget: markdown } + - label: Address + widget: object + name: address + fields: + - { label: Business name, name: name, widget: string, required: false } + - { label: Address, name: location, widget: string, required: false } + - { label: Zipcode, name: zipcode, widget: string, required: false } + - { label: City, name: city, widget: string, required: false } + - { label: Country, name: country, widget: string, required: false } + - { label: Displayed, name: displayed, widget: boolean, default: true } + - label: Projects + name: projects + widget: list + fields: + - { label: Project name, name: name, widget: string } + - { + label: Displayed, + name: displayed, + widget: boolean, + default: true, + } + - { label: Description, name: description, widget: markdown } + - label: Image + name: image + widget: object + fields: + - { + label: Source, + name: source, + widget: image, + required: false, + } + - { + label: Image description, + name: image_description, + widget: string, + required: false, + } + - label: Presentations + name: presentations + widget: list + fields: + - { label: Name, name: name, widget: string } + - { + label: Displayed, + name: displayed, + widget: boolean, + default: true, + } + - { label: Description, name: description, widget: markdown } + - { label: Link, name: link, widget: string } + - label: Education + name: education + widget: list + fields: + - { label: Institution, name: name, widget: string } + - { + label: Displayed, + name: displayed, + widget: boolean, + default: true, + } + - { label: Description, name: description, widget: markdown }