Compare commits
34 Commits
b0e49a904a
...
misc/migra
Author | SHA1 | Date | |
---|---|---|---|
6a5a9c890f | |||
5ba193cd56 | |||
95b105762d | |||
38e26ebf1f | |||
fa9b104f60 | |||
b47e8e18d0 | |||
546bf4400d | |||
134159f79c | |||
489156fe87 | |||
83a24557bd | |||
facb304a52 | |||
1e8f48b6fe | |||
1473b676f4 | |||
74f875460a | |||
cd639a830b | |||
cde3faa3c6 | |||
e8f9ecc241 | |||
1a059a005a | |||
ff087b0577 | |||
13820f58eb | |||
f7eb6cc95d | |||
fec60900f5 | |||
96ead1a38f | |||
0509565c2b | |||
0b3da60cad | |||
0228429ad0 | |||
39596cb39c | |||
55c8f29b24 | |||
289763baec | |||
5338dcd0ba | |||
921b2a493f | |||
e1384d1812 | |||
39b976b182 | |||
34b67d9ac1 |
@ -13,7 +13,7 @@ jobs:
|
||||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:18
|
||||
image: node:22
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,3 +23,6 @@ Cargo.lock
|
||||
|
||||
# Image generator
|
||||
generated_images/
|
||||
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
@ -1,29 +1,26 @@
|
||||
layout {
|
||||
pane split_direction="vertical" focus=true {
|
||||
pane edit="src/main.rs"
|
||||
pane split_direction="horizontal" size=60 {
|
||||
just { args "server_dev"; }
|
||||
just { args "test"; }
|
||||
default_tab_template {
|
||||
children
|
||||
pane size=2 borderless=true {
|
||||
plugin location="zellij:status-bar"
|
||||
}
|
||||
pane size=1 borderless=true {
|
||||
plugin location="zellij:tab-bar"
|
||||
}
|
||||
}
|
||||
pane_template name="just" {
|
||||
command "just"
|
||||
start_suspended true
|
||||
}
|
||||
floating_panes {
|
||||
pane {
|
||||
command "just"
|
||||
args "tailwind"
|
||||
|
||||
tab split_direction="vertical" focus=true {
|
||||
pane {
|
||||
just { args "server_dev"; }
|
||||
just { args "test"; }
|
||||
}
|
||||
pane {
|
||||
command "just"
|
||||
args "decap_server"
|
||||
pane {
|
||||
just { args "tailwind"; }
|
||||
just { args "decap_server"; }
|
||||
}
|
||||
}
|
||||
pane size=2 borderless=true {
|
||||
plugin location="zellij:status-bar"
|
||||
}
|
||||
pane size=1 borderless=true {
|
||||
plugin location="zellij:tab-bar"
|
||||
}
|
||||
tab
|
||||
}
|
||||
|
33
Cargo.toml
33
Cargo.toml
@ -6,17 +6,16 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.12", features = ["with-axum", "mime", "mime_guess"] }
|
||||
askama_axum = "0.4.0"
|
||||
axum = "0.7.3"
|
||||
askama = { version = "0.14" }
|
||||
axum = "0.8.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
pulldown-cmark = { version = "0.10" }
|
||||
pulldown-cmark = { version = "0.13" }
|
||||
gray_matter = "0.2.6"
|
||||
rss = "2.0.7"
|
||||
serde = "1.0.195"
|
||||
serde_json = "1.0.111"
|
||||
tokio = { version = "1.35.1", features = ["full"] }
|
||||
tower-http = { version = "0.5.0", features = ["trace", "fs"] }
|
||||
tower-http = { version = "0.6.0", features = ["trace", "fs"] }
|
||||
tower-livereload = "0.9.2"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
@ -25,24 +24,8 @@ anyhow = "1.0.86"
|
||||
rayon = "1.10.0"
|
||||
syntect = "5.2.0"
|
||||
indoc = "2.0.5"
|
||||
askama_escape = "0.10.3"
|
||||
askama_escape = "0.13.0"
|
||||
mime_guess = "2.0.5"
|
||||
|
||||
[build]
|
||||
rustflags = ["-Z", "threads=8"]
|
||||
|
||||
# [target.x86_64-unknown-linux-gnu]
|
||||
# rustflags = [
|
||||
# "-C", "link-arg=-fuse-ld=lld"
|
||||
# ]
|
||||
|
||||
[profile.dev]
|
||||
debug = true
|
||||
opt-level = 0
|
||||
# codegen-units = 16
|
||||
# lto = "thin"
|
||||
panic = "unwind"
|
||||
strip = false
|
||||
incremental = true
|
||||
|
||||
[profile.dev.package.askama_derive]
|
||||
opt-level = 3
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
|
102
README.md
102
README.md
@ -1,88 +1,44 @@
|
||||
# sapper-template
|
||||
# michalvanko.dev site
|
||||
|
||||
The default [Sapper](https://github.com/sveltejs/sapper) template, with branches for Rollup and webpack. To clone it and get started:
|
||||
This is the repository for my own site hosted at https://michalvanko.dev
|
||||
|
||||
```bash
|
||||
# for Rollup
|
||||
npx degit "sveltejs/sapper-template#rollup" my-app
|
||||
# for webpack
|
||||
npx degit "sveltejs/sapper-template#webpack" my-app
|
||||
cd my-app
|
||||
npm install # or yarn!
|
||||
npm run dev
|
||||
```
|
||||
Feel free to use and ammend to code to your needs.
|
||||
Respect the [Creative Commons BY-NC-ND 4.0 License](https://creativecommons.org/licenses/by-nc-nd/4.0/?ref=chooser-v1) for the content of the site.
|
||||
|
||||
Open up [localhost:3000](http://localhost:3000) and start clicking around.
|
||||
## Architecture
|
||||
|
||||
Consult [sapper.svelte.dev](https://sapper.svelte.dev) for help getting started.
|
||||
The site is hosted as a static generated HTML files on the server via [Caddy](https://caddyserver.com/) reverse proxy. There is an [example Caddyfile](./Caddyfile-preview) that can be used for deployment on server.
|
||||
During development the axum web framework serves content as a HTTP server in a classic SSR HTML.
|
||||
|
||||
### Development
|
||||
|
||||
## Structure
|
||||
Look at the [justfile](./justfile) for the available commands that are being used for development and deployment.
|
||||
|
||||
Sapper expects to find two directories in the root of your project — `src` and `static`.
|
||||
Use `just server_dev` or `just dev` for running the server for development purpose.
|
||||
|
||||
### Tools and libraries used for generating the content
|
||||
|
||||
### src
|
||||
- [Decap CMS](https://decapcms.org/)
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
- [axum web framework](https://github.com/tokio-rs/axum)
|
||||
- [tailwind](https://tailwindcss.com/)
|
||||
- [wget](https://www.gnu.org/software/wget/) for SSG
|
||||
|
||||
The [src](src) directory contains the entry points for your app — `client.js`, `server.js` and (optionally) a `service-worker.js` — along with a `template.html` file and a `routes` directory.
|
||||
### Deployment
|
||||
|
||||
Deployment requires these steps:
|
||||
|
||||
#### src/routes
|
||||
1. Ensure all images are generated
|
||||
1.1 Run server in either `dev` or `production` mode `just prod`
|
||||
1.2 Crawl the site with `just ssg` command to ensure all routes are being hit to indicate that all images have to be generated.
|
||||
1.3 Wait till the server stops generating images. Monitor the CPU load until it drops. Takes few minutes.
|
||||
2. `just export` will start the server in `production` mode and use `wget` to recursively crawl the site. Remember, content has to be linked somewhere on the site to be discovered by `wget`.
|
||||
3. `just deploy` will synchronise the `/dist` folder with the server with `rsync`
|
||||
|
||||
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
|
||||
### Image generation
|
||||
|
||||
**Pages** are Svelte components written in `.svelte` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
|
||||
I want all images to be served to users optimally.
|
||||
All images that are used are generated in several sizes so they are optimized for different displays sizes.
|
||||
Browsers will pick and download the appropriate size.
|
||||
|
||||
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
|
||||
|
||||
There are three simple rules for naming the files that define your routes:
|
||||
|
||||
* A file called `src/routes/about.svelte` corresponds to the `/about` route. A file called `src/routes/blog/[slug].svelte` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
|
||||
* The file `src/routes/index.svelte` (or `src/routes/index.js`) corresponds to the root of your app. `src/routes/about/index.svelte` is treated the same as `src/routes/about.svelte`.
|
||||
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `src/routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
|
||||
|
||||
|
||||
### static
|
||||
|
||||
The [static](static) directory contains any static assets that should be available. These are served using [sirv](https://github.com/lukeed/sirv).
|
||||
|
||||
In your [service-worker.js](app/service-worker.js) file, you can import these as `files` from the generated manifest...
|
||||
|
||||
```js
|
||||
import { files } from '@sapper/service-worker';
|
||||
```
|
||||
|
||||
...so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
|
||||
|
||||
|
||||
## Bundler config
|
||||
|
||||
Sapper uses Rollup or webpack to provide code-splitting and dynamic imports, as well as compiling your Svelte components. With webpack, it also provides hot module reloading. As long as you don't do anything daft, you can edit the configuration files to add whatever plugins you'd like.
|
||||
|
||||
|
||||
## Production mode and deployment
|
||||
|
||||
To start a production version of your app, run `npm run build && npm start`. This will disable live reloading, and activate the appropriate bundler plugins.
|
||||
|
||||
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
|
||||
|
||||
```bash
|
||||
npm install -g now
|
||||
now
|
||||
```
|
||||
|
||||
|
||||
## Using external components
|
||||
|
||||
When using Svelte components installed from npm, such as [@sveltejs/svelte-virtual-list](https://github.com/sveltejs/svelte-virtual-list), Svelte needs the original component source (rather than any precompiled JavaScript that ships with the component). This allows the component to be rendered server-side, and also keeps your client-side app smaller.
|
||||
|
||||
Because of that, it's essential that the bundler doesn't treat the package as an *external dependency*. You can either modify the `external` option under `server` in [rollup.config.js](rollup.config.js) or the `externals` option in [webpack.config.js](webpack.config.js), or simply install the package to `devDependencies` rather than `dependencies`, which will cause it to get bundled (and therefore compiled) with your app:
|
||||
|
||||
```bash
|
||||
npm install -D @sveltejs/svelte-virtual-list
|
||||
```
|
||||
|
||||
|
||||
## Bugs and feedback
|
||||
|
||||
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
|
||||
I'd love to link some references for this problem, but I haven't found the exact use case that I was trying to solve.
|
||||
|
@ -3,7 +3,6 @@ layout: blog
|
||||
title: Introduction to JavaScript Application testing
|
||||
segments:
|
||||
- blog
|
||||
- featured
|
||||
published: true
|
||||
date: 2021-05-07T14:44:57.102Z # update date accordingly
|
||||
tags:
|
||||
|
@ -1,7 +1,6 @@
|
||||
---
|
||||
segments:
|
||||
- blog
|
||||
- featured
|
||||
notes: ""
|
||||
layout: blog
|
||||
title: "Our attempt at Rusty game jam - Weekly #25-2022"
|
||||
|
34
_posts/blog/2024-11-08-my-first-published-cargo-crate.md
Normal file
34
_posts/blog/2024-11-08-my-first-published-cargo-crate.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
title: My first published Cargo crate
|
||||
segments:
|
||||
- blog
|
||||
- featured
|
||||
published: true
|
||||
date: 2024-11-08T17:09:00.000Z
|
||||
thumbnail: /images/uploads/aperture-icon.svg
|
||||
tags:
|
||||
- News
|
||||
- Development
|
||||
- Rust
|
||||
---
|
||||
I've become obsessed with cameras, during our Tenerife trip, where I got to use a borrowed mirrorless camera for the first time. After that, I researched which camera I'd buy for myself for five months. While watching all the reviews, I was interested in the **exposure settings** for each picture presented. I've displayed many photographs in our [Tenerife vlog with my wife](https://www.youtube.com/watch?v=tEpoVHQW4Qs&list=PLjUl8tFKyR8rCsckLn93PAwQg6tf0cyBl). I've decided that I'd like to show those exposure settings in my other videos as they might **inspire other photographers**.
|
||||
|
||||
I've tried many image editors and tools, but not one was able to **fulfill my demands** on how I'd like to display those settings underneath each picture. Many **tools supported** some kind of **framing**, however it was always **in a limited way**.
|
||||
|
||||
## Hello `metaframer`
|
||||
|
||||
I knew straight away that this would be a cool and **small side-project** that I could create. I could use my **favorite technologies** and have good **content for my streams**.
|
||||
|
||||
At first, I thought, that I would have to **render the text** right **into the image**. It will however be in some aspect a **deformation to the quality** of the original image. There was also a question of **how should the text be displayed** in a way that for **any image size**, the text will be of a similar or determinable size.
|
||||
|
||||
Instead of **rendering pixels** into every different format I've opted for creating an **SVG file** that sits **alongside the original image**. This way the text also **scales with the image** itself. The complexity was lowered instantly. Rendering SVG is just like rendering HTML. I've used [handlebars template parser](https://handlebarsjs.com/). There are more favorable template parsers in Rust like [Askama](https://djc.github.io/askama/askama.html). But I wanted to allow users to **create their own templates** so the templates are not part of the executables and they can be stored in the user's `.config` folder. I had to test the **support of my video editing software** (I use [Kdenlive](https://kdenlive.org/) BTW) and the decision for SVG was set in stone (a.k.a. [README](https://github.com/michalvankodev/metaframer)).
|
||||
|
||||
There were still complications along the way. **SVG** is still a much **less capable format than HTML** and **positioning elements** in a `space-around` fashion **has to be calculated**. There are also many different options and resolutions that I had to consider and I put all of them into the CLI arguments with [clap](https://docs.rs/clap/latest/clap/). [Clap](https://docs.rs/clap/latest/clap/) is a fantastic **command line argument parser** for Rust. I had to do many calculations on the resolutions and whether the image was a portrait or not. In the end, It wasn't as small of a side-project as I thought. I'm still very proud of it.
|
||||
|
||||
I got to experience the ease of publishing crates on [crates.io](https://crates.io/) with [cargo](https://doc.rust-lang.org/cargo/guide/).
|
||||
|
||||
This week, I've **automatized the [release process](https://github.com/michalvankodev/metaframer/blob/main/.github/workflows/release.yml)** and published binaries for multiple platforms.
|
||||
|
||||
## On to the next one
|
||||
|
||||
I can't wait to use [`metaframer`](https://github.com/michalvankodev/metaframer) in my new videos. I've already **ordered my first mirrorless camera** and it is on the way. **Be ready for new content**. What should be my next project?
|
35
_posts/blog/2024-11-23-autocommit-push-git-repositories.md
Normal file
35
_posts/blog/2024-11-23-autocommit-push-git-repositories.md
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Auto-commit & push git repositories
|
||||
segments:
|
||||
- blog
|
||||
published: true
|
||||
date: 2024-11-23T15:23:00.000Z
|
||||
thumbnail: /images/uploads/git-icon-logo-svg-vector.svg
|
||||
tags:
|
||||
- News
|
||||
- Development
|
||||
- Rust
|
||||
notes: ""
|
||||
---
|
||||
## Motivation
|
||||
|
||||
I've been **writing my notes** for over 4 years in **markdown**. I write my notes in similar style of a [Zettelkasten method](https://zettelkasten.de/introduction/). I was **inspired by [foambubble](https://github.com/foambubble/foam) project** and was using it for quite some time. The goal of foambubble was to re-use as much **free technology** for writing and **keeping second brain**. The author [Jani Eväkallio](https://jevakallio.dev/) has chosen to write **a plugin for VSCode** and **utilize git workflow**.
|
||||
This approach as complicated for average human was very appealing to me as I was already user of certain tools.
|
||||
|
||||
Since then, I stopped using VSCode as my code editor and therefore, as there wasn't support for *foam* outside of VSCode I've **migrated to a simpler workflow** using [zk](https://github.com/zk-org/zk).
|
||||
|
||||
Since then, I have struggled a lot with **forgetting to commit** my changes as I often write updates, mark completed tasks, or write new TODOs throughout the day.
|
||||
I use a computer and a laptop when on the road and it is miserable when you forget to push your changes. I'm still trying to find a perfect workflow for **synchronizing notes with my mobile phone**. I've used many different applications, but they **all failed** me as soon as there was a **git conflict**. I think I have a solution for this thought through.
|
||||
|
||||
A very easy solution would be to run a [script generated with GPT](https://letmegpt.com/?q=I'd%20like%20to%20automatically%20commit%20and%20push%20to%20my%20git%20repository%20after%205%20minutes%20of%20inactivity%20on%20my%20changes.) to **auto-commit**. It has the downsides of always having to remember to start it and it would not be easy to **share and apply to other repositories**. So, I came up with another **Rust crate**.
|
||||
|
||||
## Welcome `git_afk`
|
||||
|
||||
`git_afk` might stand for *get always forgotten knowledge*. It doesn't. I've **chosen Rust** again. As I use it more frequently, my appreciation for it grows. I feel like Rust just makes you manage the state in the place where the state belongs. Working on this project taught me how to keep and share asynchronous callbacks in a way that makes sense but isn't so obvious at first glance. The **compiler is always very helpful**. While in other languages you would need to keep `unsubscribe` handles all over the place to **avoid memory leaks**. Rust makes you think differently about how to structure the code and data around it.
|
||||
|
||||
The application is a **CLI app** that has a basic ability to add repositories to the watch list and then run `watch` command.
|
||||
Anytime there is a change in the configuration file it will reload watchers and start watching all files in the repositories for changes. It uses [`inotify`](https://www.man7.org/linux/man-pages/man7/inotify.7.html) a kernel interface for watching. You can find the [source code on GitHub](https://github.com/michalvankodev/git_afk).
|
||||
|
||||
As the Rust ecosystem is so simple you can install `git_afk` with [`cargo`](https://crates.io/) `cargo install git_afk`. Submitting packages is easy. Installing them as well. **Automatic documentation generation** is awesome. Every package has the same style of documentation. Very easy to navigate. Rust is growing up on me. I'd very much like to use it as my main tool for my job.
|
||||
|
||||
Now I can safely *git away from keyboard*.
|
2
justfile
2
justfile
@ -2,7 +2,7 @@ port := env_var_or_default('PORT', '3080')
|
||||
|
||||
# Tailwind in watch mode
|
||||
tailwind:
|
||||
npx tailwindcss -i ./styles/input.css -o ./styles/output.css --watch
|
||||
npx @tailwindcss/cli -i ./styles/input.css -o ./styles/output.css --watch
|
||||
|
||||
# svg sprite creation
|
||||
svgstore:
|
||||
|
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.1.10",
|
||||
"tailwindcss": "^4.1.10"
|
||||
}
|
||||
}
|
38
src/feed.rs
38
src/feed.rs
@ -1,11 +1,21 @@
|
||||
use askama::Values;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::Utc;
|
||||
use rss::{ChannelBuilder, GuidBuilder, Item, ItemBuilder};
|
||||
use rss::{ChannelBuilder, EnclosureBuilder, GuidBuilder, Item, ItemBuilder};
|
||||
|
||||
use crate::blog_posts::blog_post_model::{BlogPostMetadata, BLOG_POST_PATH};
|
||||
use crate::filters::{parse_markdown, truncate_md};
|
||||
use crate::post_utils::post_listing::get_post_list;
|
||||
|
||||
struct EmptyValues;
|
||||
|
||||
impl Values for EmptyValues {
|
||||
fn get_value<'a>(&'a self, _key: &str) -> Option<&'a dyn std::any::Any> {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
|
||||
let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
||||
.await
|
||||
@ -25,8 +35,30 @@ pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
|
||||
ItemBuilder::default()
|
||||
.title(Some(post.metadata.title))
|
||||
.link(Some(format!("https://michalvanko.dev/blog/{}", post.slug)))
|
||||
// TODO Description should be just a preview
|
||||
.description(None)
|
||||
.description({
|
||||
let truncated = truncate_md(&post.body, &EmptyValues, 2)
|
||||
.unwrap_or("Can't parse post body".to_string());
|
||||
let parsed_md = parse_markdown(&truncated, &EmptyValues)
|
||||
.unwrap_or("Can't process truncated post body".to_string());
|
||||
Some(parsed_md)
|
||||
})
|
||||
.content({
|
||||
let parsed_md = parse_markdown(&post.body, &EmptyValues)
|
||||
.unwrap_or("Can't process full post body".to_string());
|
||||
Some(parsed_md)
|
||||
})
|
||||
.enclosure({
|
||||
post.metadata.thumbnail.map(|src| {
|
||||
let mime_type = mime_guess::from_path(&src)
|
||||
.first()
|
||||
.map(|mime| mime.to_string())
|
||||
.unwrap_or("image".to_string());
|
||||
EnclosureBuilder::default()
|
||||
.url(src)
|
||||
.mime_type(mime_type)
|
||||
.build()
|
||||
})
|
||||
})
|
||||
.guid(Some(
|
||||
GuidBuilder::default()
|
||||
.value(format!("https://michalvanko.dev/blog/{}", post.slug))
|
||||
|
@ -1,3 +1,4 @@
|
||||
use core::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use image::image_dimensions;
|
||||
@ -19,7 +20,10 @@ enum TextKind {
|
||||
}
|
||||
|
||||
// pub fn parse_markdown(markdown: &str) -> ::askama::Result<String>
|
||||
pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> {
|
||||
pub fn parse_markdown<T: fmt::Display>(
|
||||
markdown: T,
|
||||
_: &dyn askama::Values,
|
||||
) -> ::askama::Result<String> {
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_TABLES);
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
@ -34,7 +38,8 @@ pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> {
|
||||
let theme = theme_set.themes.get("InspiredGitHub").unwrap();
|
||||
let mut heading_ended: Option<bool> = None;
|
||||
|
||||
let parser = Parser::new_ext(markdown, options).map(|event| match event {
|
||||
let mds = markdown.to_string();
|
||||
let parser = Parser::new_ext(&mds, options).map(|event| match event {
|
||||
/*
|
||||
Parsing images considers `alt` attribute as inner `Text` event
|
||||
Therefore the `[alt]` is rendered in html as subtitle
|
||||
|
@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
// This filter does not have extra arguments
|
||||
pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> {
|
||||
pub fn pretty_date(date_time: &DateTime<Utc>, _: &dyn askama::Values) -> ::askama::Result<String> {
|
||||
let formatted = format!("{}", date_time.format("%e %B %Y"));
|
||||
Ok(formatted)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"];
|
||||
|
||||
pub fn truncate_md(body: &str, rows: usize) -> ::askama::Result<String> {
|
||||
pub fn truncate_md(body: &str, _: &dyn askama::Values, rows: usize) -> ::askama::Result<String> {
|
||||
let description = body
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
|
@ -56,6 +56,5 @@ async fn main() {
|
||||
// - fotos
|
||||
// THINK deploy to alula? rather then katelyn? can be change whenever
|
||||
//
|
||||
// TODO view page transitions
|
||||
// TODO cookbook
|
||||
// TODO remove m-logo-svg from justfile and mention it in some article!!! WRITE SOME NEW ARTICLES
|
||||
// TODO remove m-logo-svg from justfile and mention it in some article!!!
|
||||
|
@ -1,9 +1,17 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "admin.html")]
|
||||
pub struct AdminPageTemplate {}
|
||||
|
||||
pub async fn render_admin() -> AdminPageTemplate {
|
||||
AdminPageTemplate {}
|
||||
pub async fn render_admin() -> Result<impl IntoResponse, StatusCode> {
|
||||
Ok(Html(
|
||||
AdminPageTemplate {}
|
||||
.render()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
|
||||
))
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
use askama::Template;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "assets/animated_logo.html")]
|
||||
pub struct AnimatedLogoTemplate {}
|
||||
|
||||
pub async fn render_animated_logo() -> Result<AnimatedLogoTemplate, StatusCode> {
|
||||
Ok(AnimatedLogoTemplate {})
|
||||
pub async fn render_animated_logo() -> Result<impl IntoResponse, StatusCode> {
|
||||
Ok(Html(AnimatedLogoTemplate {}.render().unwrap()))
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{OriginalUri, Path},
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use tokio::try_join;
|
||||
use tracing::debug;
|
||||
@ -22,7 +24,7 @@ use super::post_list::PostListTemplate;
|
||||
pub async fn render_blog_post_list(
|
||||
tag: Option<Path<String>>,
|
||||
OriginalUri(original_uri): OriginalUri,
|
||||
) -> Result<PostListTemplate, StatusCode> {
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
||||
let tag = tag.map(|Path(tag)| tag);
|
||||
|
||||
@ -50,14 +52,18 @@ pub async fn render_blog_post_list(
|
||||
("Blog posts".to_string(), "Blog posts".to_string())
|
||||
};
|
||||
|
||||
Ok(PostListTemplate {
|
||||
title,
|
||||
og_title,
|
||||
segment: Segment::Blog,
|
||||
posts,
|
||||
header_props,
|
||||
tags: blog_tags,
|
||||
featured_projects,
|
||||
current_url: original_uri.to_string(),
|
||||
})
|
||||
Ok(Html(
|
||||
PostListTemplate {
|
||||
title,
|
||||
og_title,
|
||||
segment: Segment::Blog,
|
||||
posts,
|
||||
header_props,
|
||||
tags: blog_tags,
|
||||
featured_projects,
|
||||
current_url: original_uri.to_string(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
use askama::Template;
|
||||
use axum::extract::OriginalUri;
|
||||
use axum::response::{Html, IntoResponse};
|
||||
use axum::{extract::Path, http::StatusCode};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::blog_posts::blog_post_model::BLOG_POST_PATH;
|
||||
use crate::blog_posts::blog_post_model::{Segment, BLOG_POST_PATH};
|
||||
use crate::post_utils::post_listing::get_post_list;
|
||||
use crate::post_utils::post_parser::ParseResult;
|
||||
use crate::{
|
||||
blog_posts::blog_post_model::BlogPostMetadata, components::site_header::Link, filters,
|
||||
post_utils::post_parser::parse_post,
|
||||
@ -18,47 +21,84 @@ pub struct BlogPostTemplate {
|
||||
pub body: String,
|
||||
pub date: DateTime<Utc>,
|
||||
pub tags: Vec<String>,
|
||||
pub segment: String,
|
||||
pub segment: Segment,
|
||||
pub header_props: HeaderProps,
|
||||
pub slug: String,
|
||||
pub thumbnail: Option<String>,
|
||||
pub recommended_posts: Vec<ParseResult<BlogPostMetadata>>,
|
||||
}
|
||||
|
||||
pub async fn render_blog_post(
|
||||
Path(post_id): Path<String>,
|
||||
OriginalUri(original_uri): OriginalUri,
|
||||
) -> Result<BlogPostTemplate, StatusCode> {
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
|
||||
let parse_post = parse_post::<BlogPostMetadata>(&path);
|
||||
let parsed = parse_post.await?;
|
||||
let post = parse_post::<BlogPostMetadata>(&path).await?;
|
||||
let segment = if original_uri.to_string().starts_with("/blog") {
|
||||
"blog"
|
||||
Segment::Blog
|
||||
} else if original_uri.to_string().starts_with("/broadcasts") {
|
||||
"broadcasts"
|
||||
Segment::Broadcasts
|
||||
} else {
|
||||
"blog"
|
||||
Segment::Blog
|
||||
};
|
||||
|
||||
let mut recommended_posts = get_recommended_posts(&segment, &post.metadata.tags).await?;
|
||||
recommended_posts.retain(|post| post.slug != post_id);
|
||||
recommended_posts.sort_by_key(|post| post.slug.to_string());
|
||||
recommended_posts.reverse();
|
||||
let recommended_posts = recommended_posts.into_iter().take(2).collect::<Vec<_>>();
|
||||
|
||||
let header_props = match segment {
|
||||
"blog" => HeaderProps::with_back_link(Link {
|
||||
Segment::Blog => HeaderProps::with_back_link(Link {
|
||||
href: "/blog".to_string(),
|
||||
label: "All posts".to_string(),
|
||||
}),
|
||||
"broadcasts" => HeaderProps::with_back_link(Link {
|
||||
Segment::Broadcasts => HeaderProps::with_back_link(Link {
|
||||
href: "/broadcasts".to_string(),
|
||||
label: "All broadcasts".to_string(),
|
||||
}),
|
||||
_ => HeaderProps::default(),
|
||||
};
|
||||
|
||||
Ok(BlogPostTemplate {
|
||||
title: parsed.metadata.title,
|
||||
date: parsed.metadata.date,
|
||||
tags: parsed.metadata.tags,
|
||||
body: parsed.body,
|
||||
slug: parsed.slug,
|
||||
segment: segment.to_string(),
|
||||
thumbnail: parsed.metadata.thumbnail,
|
||||
header_props,
|
||||
})
|
||||
Ok(Html(
|
||||
BlogPostTemplate {
|
||||
title: post.metadata.title,
|
||||
date: post.metadata.date,
|
||||
tags: post.metadata.tags,
|
||||
body: post.body,
|
||||
slug: post.slug,
|
||||
segment,
|
||||
thumbnail: post.metadata.thumbnail,
|
||||
header_props,
|
||||
recommended_posts,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_recommended_posts(
|
||||
segment: &Segment,
|
||||
tags: &[String],
|
||||
) -> Result<Vec<ParseResult<BlogPostMetadata>>, StatusCode> {
|
||||
let posts = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH).await?;
|
||||
|
||||
let recommended_posts = posts
|
||||
.into_iter()
|
||||
.filter(|post| {
|
||||
let is_same_segment = post
|
||||
.metadata
|
||||
.segments
|
||||
.iter()
|
||||
.any(|post_segment| post_segment == segment);
|
||||
let has_same_tags = post
|
||||
.metadata
|
||||
.tags
|
||||
.iter()
|
||||
.any(|post_tag| tags.contains(post_tag));
|
||||
is_same_segment && has_same_tags
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(recommended_posts)
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::{OriginalUri, Path},
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use tokio::try_join;
|
||||
use tracing::debug;
|
||||
@ -21,7 +23,7 @@ use super::post_list::PostListTemplate;
|
||||
pub async fn render_broadcast_post_list(
|
||||
tag: Option<Path<String>>,
|
||||
OriginalUri(original_uri): OriginalUri,
|
||||
) -> Result<PostListTemplate, StatusCode> {
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
||||
let tag = tag.map(|Path(tag)| tag);
|
||||
|
||||
@ -50,14 +52,18 @@ pub async fn render_broadcast_post_list(
|
||||
"Broadcasts".to_string()
|
||||
};
|
||||
|
||||
Ok(PostListTemplate {
|
||||
title: title.clone(),
|
||||
og_title: title,
|
||||
segment: Segment::Broadcasts,
|
||||
posts,
|
||||
header_props,
|
||||
tags: popular_tags,
|
||||
featured_projects,
|
||||
current_url: original_uri.to_string(),
|
||||
})
|
||||
Ok(Html(
|
||||
PostListTemplate {
|
||||
title: title.clone(),
|
||||
og_title: title,
|
||||
segment: Segment::Broadcasts,
|
||||
posts,
|
||||
header_props,
|
||||
tags: popular_tags,
|
||||
featured_projects,
|
||||
current_url: original_uri.to_string(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
use askama::Template;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
|
||||
use crate::components::site_header::HeaderProps;
|
||||
|
||||
@ -18,7 +21,7 @@ pub struct ContactPageTemplate {
|
||||
pub links: Vec<ContactLink>,
|
||||
}
|
||||
|
||||
pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
|
||||
pub async fn render_contact() -> Result<impl IntoResponse, StatusCode> {
|
||||
let links = vec![
|
||||
ContactLink {
|
||||
href: "mailto:michalvankosk@gmail.com".to_string(),
|
||||
@ -44,6 +47,12 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
|
||||
title: "YouTube channel".to_string(),
|
||||
svg: "youtube".to_string(),
|
||||
},
|
||||
ContactLink {
|
||||
href: "https://mastodon.online/@michalvankodev".to_string(),
|
||||
label: "Mastodon".to_string(),
|
||||
title: "Mastodon profile".to_string(),
|
||||
svg: "mastodon".to_string(),
|
||||
},
|
||||
ContactLink {
|
||||
href: "https://instagram.com/michalvankodev".to_string(),
|
||||
label: "Instagram".to_string(),
|
||||
@ -70,9 +79,13 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
|
||||
},
|
||||
];
|
||||
|
||||
Ok(ContactPageTemplate {
|
||||
title: "Contact".to_owned(),
|
||||
header_props: HeaderProps::default(),
|
||||
links,
|
||||
})
|
||||
Ok(Html(
|
||||
ContactPageTemplate {
|
||||
title: "Contact".to_owned(),
|
||||
header_props: HeaderProps::default(),
|
||||
links,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use askama::Template;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use tokio::try_join;
|
||||
|
||||
use crate::{
|
||||
@ -26,7 +29,7 @@ pub struct IndexTemplate {
|
||||
featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>,
|
||||
}
|
||||
|
||||
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
||||
pub async fn render_index() -> Result<impl IntoResponse, StatusCode> {
|
||||
let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!(
|
||||
get_popular_tags(Some(Segment::Blog)),
|
||||
get_popular_tags(Some(Segment::Broadcasts)),
|
||||
@ -44,12 +47,16 @@ pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
||||
let featured_broadcasts =
|
||||
ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]);
|
||||
|
||||
Ok(IndexTemplate {
|
||||
header_props: HeaderProps::default(),
|
||||
blog_tags,
|
||||
broadcasts_tags,
|
||||
featured_blog_posts,
|
||||
featured_projects,
|
||||
featured_broadcasts,
|
||||
})
|
||||
Ok(Html(
|
||||
IndexTemplate {
|
||||
header_props: HeaderProps::default(),
|
||||
blog_tags,
|
||||
broadcasts_tags,
|
||||
featured_blog_posts,
|
||||
featured_projects,
|
||||
featured_broadcasts,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
use askama::Template;
|
||||
use axum::{extract::OriginalUri, http::StatusCode};
|
||||
use axum::{
|
||||
extract::OriginalUri,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
use crate::components::site_header::HeaderProps;
|
||||
@ -13,13 +17,17 @@ pub struct NotFoundPage {
|
||||
|
||||
pub async fn render_not_found(
|
||||
OriginalUri(original_uri): OriginalUri,
|
||||
) -> Result<(StatusCode, NotFoundPage), StatusCode> {
|
||||
) -> Result<(StatusCode, impl IntoResponse), StatusCode> {
|
||||
info!("{original_uri} not found");
|
||||
Ok((
|
||||
StatusCode::NOT_FOUND,
|
||||
NotFoundPage {
|
||||
title: "This page does not exists".to_owned(),
|
||||
header_props: HeaderProps::default(),
|
||||
},
|
||||
Html(
|
||||
NotFoundPage {
|
||||
title: "This page does not exists".to_owned(),
|
||||
header_props: HeaderProps::default(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
use askama::Template;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
@ -50,7 +53,7 @@ pub struct PortfolioTemplate {
|
||||
pub technology_list: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
|
||||
pub async fn render_portfolio() -> Result<impl IntoResponse, StatusCode> {
|
||||
let portfolio = parse_post::<PortfolioPageModel>("_pages/portfolio.md").await?;
|
||||
|
||||
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
|
||||
@ -126,14 +129,18 @@ pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
|
||||
.map(|str| str.to_owned())
|
||||
.collect();
|
||||
|
||||
Ok(PortfolioTemplate {
|
||||
title: "Portfolio".to_owned(),
|
||||
body: portfolio.body,
|
||||
header_props: HeaderProps::default(),
|
||||
project_list,
|
||||
workplace_list,
|
||||
education_list,
|
||||
contact_links,
|
||||
technology_list,
|
||||
})
|
||||
Ok(Html(
|
||||
PortfolioTemplate {
|
||||
title: "Portfolio".to_owned(),
|
||||
body: portfolio.body,
|
||||
header_props: HeaderProps::default(),
|
||||
project_list,
|
||||
workplace_list,
|
||||
education_list,
|
||||
contact_links,
|
||||
technology_list,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
use askama::Template;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
components::site_header::HeaderProps,
|
||||
@ -16,16 +19,20 @@ pub struct ProjectListTemplate {
|
||||
pub header_props: HeaderProps,
|
||||
}
|
||||
|
||||
pub async fn render_projects_list() -> Result<ProjectListTemplate, StatusCode> {
|
||||
pub async fn render_projects_list() -> Result<impl IntoResponse, StatusCode> {
|
||||
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
|
||||
|
||||
project_list.sort_by_key(|post| post.slug.to_string());
|
||||
project_list.retain(|project| project.metadata.displayed);
|
||||
project_list.reverse();
|
||||
|
||||
Ok(ProjectListTemplate {
|
||||
title: "Showcase".to_owned(),
|
||||
header_props: HeaderProps::default(),
|
||||
project_list,
|
||||
})
|
||||
Ok(Html(
|
||||
ProjectListTemplate {
|
||||
title: "Showcase".to_owned(),
|
||||
header_props: HeaderProps::default(),
|
||||
project_list,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
use askama::Template;
|
||||
use axum::http::StatusCode;
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
};
|
||||
|
||||
use crate::components::site_header::HeaderProps;
|
||||
|
||||
@ -9,8 +12,12 @@ pub struct EggFetcherShowcaseTemplate {
|
||||
header_props: HeaderProps,
|
||||
}
|
||||
|
||||
pub async fn render_egg_fetcher() -> Result<EggFetcherShowcaseTemplate, StatusCode> {
|
||||
Ok(EggFetcherShowcaseTemplate {
|
||||
header_props: HeaderProps::default(),
|
||||
})
|
||||
pub async fn render_egg_fetcher() -> Result<impl IntoResponse, StatusCode> {
|
||||
Ok(Html(
|
||||
EggFetcherShowcaseTemplate {
|
||||
header_props: HeaderProps::default(),
|
||||
}
|
||||
.render()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR),
|
||||
))
|
||||
}
|
||||
|
@ -262,7 +262,8 @@ pub fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> {
|
||||
fn test_get_export_formats() {
|
||||
assert_eq!(
|
||||
get_export_formats("/images/uploads/img_name.jpg"),
|
||||
vec![ExportFormat::Avif, ExportFormat::Jpeg]
|
||||
// vec![ExportFormat::Avif, ExportFormat::Jpeg]
|
||||
vec![ExportFormat::Jpeg]
|
||||
)
|
||||
}
|
||||
#[test]
|
||||
@ -292,24 +293,21 @@ fn test_generate_picture_markup() {
|
||||
let height = 200;
|
||||
let orig_img_path = "/images/uploads/2020-03-23_20-24-06_393.jpg";
|
||||
let result = indoc! {
|
||||
r#"<picture>
|
||||
<source
|
||||
srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.avif 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.avif 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.avif 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.avif 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.avif 4x"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.jpg 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.jpg 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.jpg 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.jpg 4x"
|
||||
type="image/jpeg"
|
||||
>
|
||||
<img
|
||||
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
|
||||
width="300"
|
||||
height="200"
|
||||
alt="Testing image alt"
|
||||
>
|
||||
</picture>"#,
|
||||
r#"<picture>
|
||||
<source
|
||||
srcset="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg 1x, /generated_images/images/uploads/2020-03-23_20-24-06_393_450x300.jpg 1.5x, /generated_images/images/uploads/2020-03-23_20-24-06_393_600x400.jpg 2x, /generated_images/images/uploads/2020-03-23_20-24-06_393_900x600.jpg 3x, /generated_images/images/uploads/2020-03-23_20-24-06_393_1200x800.jpg 4x"
|
||||
type="image/jpeg"
|
||||
>
|
||||
<img
|
||||
src="/generated_images/images/uploads/2020-03-23_20-24-06_393_300x200.jpg"
|
||||
width="300"
|
||||
height="200"
|
||||
alt="Testing image alt"
|
||||
|
||||
>
|
||||
</picture>"#,
|
||||
};
|
||||
assert_eq!(
|
||||
pretty_assertions::assert_eq!(
|
||||
generate_picture_markup(orig_img_path, width, height, "Testing image alt", None,)
|
||||
.expect("picture markup has to be generated"),
|
||||
result
|
||||
|
@ -8,7 +8,14 @@ use crate::{
|
||||
project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher,
|
||||
},
|
||||
};
|
||||
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
extract::MatchedPath,
|
||||
http::{Request, StatusCode},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::info_span;
|
||||
|
||||
@ -16,15 +23,15 @@ pub fn get_router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(render_index))
|
||||
.route("/blog", get(render_blog_post_list))
|
||||
.route("/blog/tags/:tag", get(render_blog_post_list))
|
||||
.route("/blog/:post_id", get(render_blog_post))
|
||||
.route("/blog/tags/{tag}", get(render_blog_post_list))
|
||||
.route("/blog/{post_id}", get(render_blog_post))
|
||||
.route("/broadcasts", get(render_broadcast_post_list))
|
||||
.route("/broadcasts/tags/:tag", get(render_broadcast_post_list))
|
||||
.route("/broadcasts/:post_id", get(render_blog_post))
|
||||
.route("/broadcasts/tags/{tag}", get(render_broadcast_post_list))
|
||||
.route("/broadcasts/{post_id}", get(render_blog_post))
|
||||
.route("/contact", get(render_contact))
|
||||
.route("/showcase", get(render_projects_list))
|
||||
.route("/showcase/m-logo-svg", get(render_animated_logo))
|
||||
.route("/showcase/:project_slug", get(render_egg_fetcher))
|
||||
.route("/showcase/{project_slug}", get(render_egg_fetcher))
|
||||
.route("/portfolio", get(render_portfolio))
|
||||
.route("/admin", get(render_admin))
|
||||
.route("/feed.xml", get(render_rss_feed))
|
||||
|
3
static/images/uploads/aperture-icon.svg
Normal file
3
static/images/uploads/aperture-icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg height="{{height}}" width="{{width}}" x="{{x}}" y="{{y}}" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h48v48h-48z" fill="none"/><path d="M18.8 21l9.53-16.51c-1.39-.31-2.84-.49-4.33-.49-4.8 0-9.19 1.69-12.64 4.51l7.33 12.69.11-.2zm24.28-3c-1.84-5.85-6.3-10.52-11.99-12.68l-7.32 12.68h19.31zm.52 2h-14.98l.58 1 9.53 16.5c3.26-3.56 5.27-8.29 5.27-13.5 0-1.37-.14-2.71-.4-4zm-26.53 4l-7.8-13.5c-3.26 3.56-5.27 8.29-5.27 13.5 0 1.37.14 2.71.4 4h14.98l-2.31-4zm-12.15 6c1.84 5.85 6.3 10.52 11.99 12.68l7.32-12.68h-19.31zm22.54 0l-7.8 13.51c1.4.31 2.85.49 4.34.49 4.8 0 9.19-1.69 12.64-4.51l-7.33-12.69-1.85 3.2z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 650 B |
1
static/images/uploads/git-icon-logo-svg-vector.svg
Normal file
1
static/images/uploads/git-icon-logo-svg-vector.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="2500" height="2500" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><path d="M251.172 116.594L139.4 4.828c-6.433-6.437-16.873-6.437-23.314 0l-23.21 23.21 29.443 29.443c6.842-2.312 14.688-.761 20.142 4.693 5.48 5.489 7.02 13.402 4.652 20.266l28.375 28.376c6.865-2.365 14.786-.835 20.269 4.657 7.663 7.66 7.663 20.075 0 27.74-7.665 7.666-20.08 7.666-27.749 0-5.764-5.77-7.188-14.235-4.27-21.336l-26.462-26.462-.003 69.637a19.82 19.82 0 0 1 5.188 3.71c7.663 7.66 7.663 20.076 0 27.747-7.665 7.662-20.086 7.662-27.74 0-7.663-7.671-7.663-20.086 0-27.746a19.654 19.654 0 0 1 6.421-4.281V94.196a19.378 19.378 0 0 1-6.421-4.281c-5.806-5.798-7.202-14.317-4.227-21.446L81.47 39.442l-76.64 76.635c-6.44 6.443-6.44 16.884 0 23.322l111.774 111.768c6.435 6.438 16.873 6.438 23.316 0l111.251-111.249c6.438-6.44 6.438-16.887 0-23.324" fill="#DE4C36"/></svg>
|
After Width: | Height: | Size: 899 B |
1
static/svg/input/mastodon.svg
Normal file
1
static/svg/input/mastodon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"/></svg>
|
After Width: | Height: | Size: 811 B |
162
styles/input.css
162
styles/input.css
@ -1,63 +1,113 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Baloo2", "Baloo2 Noto Fallback", "Baloo2 Fallback", "ui-sans-serif", "system-ui", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
|
||||
--text-readxl: 1.75rem;
|
||||
--text-readxl--line-height: 2.25rem;
|
||||
--text-readxl--letter-spacing: -0.015rem;
|
||||
--text-readxl--font-weight: 400;
|
||||
|
||||
--spacing-note: 60rem;
|
||||
--spacing-read: 64rem;
|
||||
--spacing-image: min(70rem, 95vw);
|
||||
--spacing-maxindex: 100rem;
|
||||
|
||||
--color-blue-50: #f1f7fe;
|
||||
--color-blue-100: #e1effd;
|
||||
--color-blue-200: #bddefa;
|
||||
--color-blue-300: #82c3f7;
|
||||
--color-blue-400: #42a6f0;
|
||||
--color-blue-500: #1789e0;
|
||||
--color-blue-600: #0a6cbf;
|
||||
--color-blue-700: #0a569a;
|
||||
--color-blue-800: #0c4980;
|
||||
--color-blue-900: #103e6a;
|
||||
--color-blue-950: #0b2746;
|
||||
|
||||
--color-pink-50: #fff4fd;
|
||||
--color-pink-100: #ffe7fb;
|
||||
--color-pink-200: #ffcff7;
|
||||
--color-pink-300: #fea6eb;
|
||||
--color-pink-400: #fc76dd;
|
||||
--color-pink-500: #f342ca;
|
||||
--color-pink-600: #d722a9;
|
||||
--color-pink-700: #b31889;
|
||||
--color-pink-800: #92166e;
|
||||
--color-pink-900: #771859;
|
||||
--color-pink-950: #500238;
|
||||
|
||||
--color-purple-50: #F8F5FC;
|
||||
--color-purple-100: #D5C2ED;
|
||||
--color-purple-200: #B28EDE;
|
||||
--color-purple-300: #8F5BCF;
|
||||
--color-purple-400: #6D30B9;
|
||||
--color-purple-500: #5F2AA2;
|
||||
--color-purple-600: #52248A;
|
||||
--color-purple-700: #441E73;
|
||||
--color-purple-800: #36185C;
|
||||
--color-purple-900: #281244;
|
||||
--color-purple-950: #1A0C2D;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Baloo2';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src:
|
||||
local('Baloo2'),
|
||||
url(/fonts/baloo2/Baloo2-Latin-Variable-wght.woff2) format('woff2');
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Baloo 2';
|
||||
font-style: normal;
|
||||
font-weight: 400 800;
|
||||
font-display: swap;
|
||||
src: url(/fonts/baloo2/Baloo2-Latin-Variable-ext-wght.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Baloo2 Fallback';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'),
|
||||
local('Arial');
|
||||
ascent-override: 111.2%;
|
||||
descent-override: 54.05%;
|
||||
line-gap-override: 0%;
|
||||
size-adjust: 96.95%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Baloo2 Noto Fallback';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Noto Sans');
|
||||
ascent-override: 88%;
|
||||
descent-override: none;
|
||||
line-gap-override: 0%;
|
||||
size-adjust: 92%;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: 'Baloo2';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src:
|
||||
local('Baloo2'),
|
||||
url(/fonts/baloo2/Baloo2-Latin-Variable-wght.woff2) format('woff2');
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Baloo 2';
|
||||
font-style: normal;
|
||||
font-weight: 400 800;
|
||||
font-display: swap;
|
||||
src: url(/fonts/baloo2/Baloo2-Latin-Variable-ext-wght.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
a {
|
||||
@apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Baloo2 Fallback';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'),
|
||||
local('Arial');
|
||||
ascent-override: 111.2%;
|
||||
descent-override: 54.05%;
|
||||
line-gap-override: 0%;
|
||||
size-adjust: 96.95%;
|
||||
strong {
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Baloo2 Noto Fallback';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Noto Sans');
|
||||
ascent-override: 88%;
|
||||
descent-override: none;
|
||||
line-gap-override: 0%;
|
||||
size-adjust: 92%;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-pink-800 underline underline-offset-2 hover:transition hover:text-blue-500;
|
||||
}
|
||||
|
||||
strong {
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
.article-body {
|
||||
h1 {
|
||||
@apply px-4 text-2xl font-semibold text-blue-900 mb-3 mt-4 max-w-read mx-auto md:text-4xl lg:text-5xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply px-4 text-xl font-semibold text-blue-900 mb-3 mt-4 max-w-read mx-auto md:text-2xl md:mb-6 md:mt-8 lg:mb-8 lg:mt-12 lg:text-4xl;
|
||||
}
|
||||
@ -82,15 +132,16 @@ strong {
|
||||
@apply p-4;
|
||||
|
||||
img {
|
||||
@apply rounded shadow-md mx-auto lg:max-w-image;
|
||||
@apply rounded-sm shadow-md mx-auto lg:max-w-image;
|
||||
}
|
||||
}
|
||||
|
||||
figcaption {
|
||||
@apply mt-2 text-center text-sm italic text-blue-800 md:text-base lg:text-lg;
|
||||
}
|
||||
|
||||
table {
|
||||
@apply text-sm mx-auto my-4 max-w-image table-auto border-collapse border-spacing-12 border border-slate-200 rounded md:text-base lg:text-xl lg:my-8;
|
||||
@apply text-sm mx-auto my-4 max-w-image table-auto border-collapse border-spacing-12 border border-slate-200 rounded-sm md:text-base lg:text-xl lg:my-8;
|
||||
}
|
||||
|
||||
thead {
|
||||
@ -119,11 +170,11 @@ strong {
|
||||
}
|
||||
|
||||
:not(pre) code {
|
||||
@apply text-pink-900 rounded border border-blue-300 px-1 py-0.5 bg-blue-100 text-sm md:text-base lg:text-xl;
|
||||
@apply text-pink-900 rounded-sm border border-blue-300 px-1 py-0.5 bg-blue-100 text-sm md:text-base lg:text-xl;
|
||||
}
|
||||
|
||||
pre code pre {
|
||||
@apply mx-2 rounded lg:mx-auto lg:text-lg shadow-sm lg:max-w-note;
|
||||
@apply mx-2 rounded-sm lg:mx-auto lg:text-lg shadow-xs lg:max-w-note;
|
||||
}
|
||||
|
||||
ul,
|
||||
@ -138,12 +189,13 @@ strong {
|
||||
ul {
|
||||
@apply list-disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
@apply list-decimal;
|
||||
}
|
||||
|
||||
iframe {
|
||||
@apply rounded shadow-md mx-auto lg:max-w-image;
|
||||
@apply rounded-sm shadow-md mx-auto lg:max-w-image;
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,4 +332,4 @@ article a {
|
||||
/* animation-duration: 5.5s; */
|
||||
/* transition: transform 5.4s ease-in-out; */
|
||||
/* opacity: 1; */
|
||||
/* } */
|
||||
/* } */
|
3733
styles/output.css
3733
styles/output.css
File diff suppressed because it is too large
Load Diff
@ -1,112 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./templates/**/**.html"],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
"Baloo2",
|
||||
"Baloo2 Noto Fallback",
|
||||
"Baloo2 Fallback",
|
||||
"ui-sans-serif",
|
||||
"system-ui",
|
||||
"sans-serif",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"Noto Color Emoji",
|
||||
],
|
||||
},
|
||||
spacing: {
|
||||
note: "60rem",
|
||||
read: "64rem",
|
||||
image: "min(70rem, 95vw)",
|
||||
maxindex: "100rem",
|
||||
},
|
||||
width: {
|
||||
note: "60rem",
|
||||
read: "64rem",
|
||||
image: "min(70rem, 95vw)",
|
||||
maxindex: "100rem",
|
||||
},
|
||||
fontSize: {
|
||||
readxl: [
|
||||
"1.75rem",
|
||||
{
|
||||
lineHeight: "2.25rem",
|
||||
letterSpacing: "-0.015em",
|
||||
fontWeight: "400",
|
||||
},
|
||||
],
|
||||
},
|
||||
colors: {
|
||||
// blue: {
|
||||
// 50: "#ecf6fe",
|
||||
// 100: "#d9edfc",
|
||||
// 200: "#b3dbf9",
|
||||
// 300: "#8ecaf6",
|
||||
// 400: "#68b8f3",
|
||||
// 500: "#42a6f0",
|
||||
// 600: "#3585c0",
|
||||
// 700: "#286490",
|
||||
// 800: "#1F4E71",
|
||||
// 900: "#173A54",
|
||||
// 950: "#0F2637",
|
||||
// },
|
||||
blue: {
|
||||
50: "#f1f7fe",
|
||||
100: "#e1effd",
|
||||
200: "#bddefa",
|
||||
300: "#82c3f7",
|
||||
400: "#42a6f0",
|
||||
500: "#1789e0",
|
||||
600: "#0a6cbf",
|
||||
700: "#0a569a",
|
||||
800: "#0c4980",
|
||||
900: "#103e6a",
|
||||
950: "#0b2746",
|
||||
},
|
||||
// pink: {
|
||||
// 50: "#FFFBFE",
|
||||
// 100: "#FFE4F9",
|
||||
// 200: "#FECEF4",
|
||||
// 300: "#FEB8EF",
|
||||
// 400: "#fea6eb",
|
||||
// 500: "#D38AC3",
|
||||
// 600: "#B476A7",
|
||||
// 700: "#96628B",
|
||||
// 800: "#774E6E",
|
||||
// 900: "#593A52",
|
||||
// 950: "#3A2636",
|
||||
// },
|
||||
pink: {
|
||||
50: "#fff4fd",
|
||||
100: "#ffe7fb",
|
||||
200: "#ffcff7",
|
||||
300: "#fea6eb",
|
||||
400: "#fc76dd",
|
||||
500: "#f342ca",
|
||||
600: "#d722a9",
|
||||
700: "#b31889",
|
||||
800: "#92166e",
|
||||
900: "#771859",
|
||||
950: "#500238",
|
||||
},
|
||||
purple: {
|
||||
50: "#F8F5FC",
|
||||
100: "#D5C2ED",
|
||||
200: "#B28EDE",
|
||||
300: "#8F5BCF",
|
||||
400: "#6D30B9",
|
||||
500: "#5F2AA2",
|
||||
600: "#52248A",
|
||||
700: "#441E73",
|
||||
800: "#36185C",
|
||||
900: "#281244",
|
||||
950: "#1A0C2D",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
@ -41,7 +41,7 @@
|
||||
bgAnimationTimeline.add({
|
||||
targets: '#m-logo #bg-gradient #bg-stop',
|
||||
offset: "0%",
|
||||
stopColor: "rgba(216, 246, 255, 1)",
|
||||
stopColor: "rgba(216, 246, 255, 0.8)",
|
||||
easing: 'easeInQuint',
|
||||
duration: 123,
|
||||
})
|
||||
@ -50,6 +50,18 @@
|
||||
offset: "100%",
|
||||
easing: 'easeOutExpo',
|
||||
duration: 333,
|
||||
complete: (animation) => {
|
||||
const target = animation.animatables[0].target
|
||||
target.setAttribute("stop-color", "rgba(216, 246, 255, 0.8)")
|
||||
anime({
|
||||
targets: '#m-logo #bg-gradient #bg-stop',
|
||||
stopColor: "rgba(216, 246, 255, 0.3)",
|
||||
easing: 'easeOutQuad',
|
||||
duration: 3333,
|
||||
direction: 'alternate',
|
||||
loop: true,
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 160)
|
||||
|
@ -16,6 +16,10 @@
|
||||
href="/feed.xml"
|
||||
/>
|
||||
|
||||
<!-- Mastodon -->
|
||||
<link rel="me" href="https://mastodon.online/@michalvankodev" />
|
||||
<meta name="fediverse:creator" content="@michalvankodev@mastodon.online">
|
||||
|
||||
{% block og_meta %}
|
||||
<meta property="og:title" content="{% block title %} {{ title }} {% endblock %} @michalvankodev" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
@ -33,7 +33,32 @@
|
||||
</article>
|
||||
|
||||
<!-- TODO: Next recommendations for reading -->
|
||||
<!-- TODO: Bact to all posts -->
|
||||
<!-- TODO: Back to all posts -->
|
||||
|
||||
{# footer #}
|
||||
<footer class="max-w-maxindex mx-auto">
|
||||
|
||||
{% if recommended_posts.len() > 0 %}
|
||||
<section id="recommended-articles">
|
||||
<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>
|
||||
<ul class="mx-5 xl:flex xl:justify-start xl:gap-10">
|
||||
{% for post in recommended_posts %}
|
||||
<li class="flex-1">
|
||||
{% include "components/blog_post_preview.html" %}
|
||||
<hr class="border-slate-300 my-5 md:my-8 xl:hidden">
|
||||
</li>
|
||||
{% if !loop.last %}
|
||||
<div class="h-auto w-0 border-l border-slate-300 hidden xl:block"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="text-center my-3 md:text-lg">
|
||||
<a href="/blog">see all blog posts</a>
|
||||
</section>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
<!-- xl:border-l xl:border-slate-300 xl:first:border-l-0 xl: -->
|
||||
|
@ -17,7 +17,7 @@
|
||||
<a rel="prefetch" href="/{{segment}}/{{post.slug}}" class="text-blue-950 visited:text-purple-700 no-underline">{{post.metadata.title}}</a>
|
||||
</h3>
|
||||
</header>
|
||||
<section class="text-base leading-5 text-slate-800 md:text-xl text-justify">{{post.body|truncate_md(2)|parse_markdown|safe}}</section>
|
||||
<section class="text-base leading-7 text-slate-800 md:text-xl text-justify">{{post.body|truncate_md(2)|parse_markdown|safe}}</section>
|
||||
<footer class="text-sm md:text-base lg:text-lg mt-3 sm:mt-0 clear-both sm:clear-none">
|
||||
<ul class="inline-block" style="view-transition-name: post_tags_{{post.slug}}">
|
||||
{% for tag in post.metadata.tags %}
|
||||
|
@ -1,18 +0,0 @@
|
||||
<section class="flex border rounded bg-white p-3">
|
||||
<aside class="flex justify-center items-center pr-3 shrink-0">
|
||||
{% match education.thumbnail %}
|
||||
{% when Some with (source) %}
|
||||
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "education cover", Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
|
||||
<figure class="mx-4 my-2">
|
||||
{{picture|safe}}
|
||||
</figure>
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
</aside>
|
||||
<section>
|
||||
<header>
|
||||
<h3 class="text-lg font-medium mb-1 md:text-2xl">{{education.name}}</h3>
|
||||
</header>
|
||||
<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{education.description|parse_markdown|safe}}</section>
|
||||
</section>
|
||||
</section>
|
@ -1,4 +1,4 @@
|
||||
<section class="border rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};">
|
||||
<section class="border border-slate-200 rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};">
|
||||
<header class="px-4 mb-3">
|
||||
<h2 class="text-xl font-semibold text-blue-900 md:text-2xl">
|
||||
{% match project.metadata.link %}
|
||||
|
@ -1,9 +1,8 @@
|
||||
<section class="flex border rounded bg-white p-3">
|
||||
<section class="flex border border-slate-200 rounded bg-white p-3">
|
||||
<aside class="flex justify-center items-center pr-3 shrink-0">
|
||||
{% match skill.thumbnail %}
|
||||
{% when Some with (source) %}
|
||||
{% let skill_name = skill.name.clone() %}
|
||||
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, format!("{skill_name} cover"), Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
|
||||
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, &format!("{} cover", skill.name), Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
|
||||
<figure class="mx-4 my-2">
|
||||
{{picture|safe}}
|
||||
</figure>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% macro social_card_start(svg, url, heading, img, class) %}
|
||||
|
||||
<a href="{{url}}" class="block no-underline border rounded-md bg-pink-200 m-4 p-4 max-w-[392px] {{class}}">
|
||||
<a href="{{url}}" class="block no-underline border border-slate-200 rounded-md bg-pink-200 m-4 p-4 max-w-[392px] {{class}}">
|
||||
<header class="flex text-center justify-center items-center gap-2 mb-2">
|
||||
<svg role="img" aria-label="{{svg}} icon" aria-hidden="true" class="h-7 w-7 fill-blue-950">
|
||||
<use href="#{{svg}}" />
|
||||
@ -9,6 +9,6 @@
|
||||
</header>
|
||||
{% let alt_text = format!("{svg} thumbnail") %}
|
||||
|
||||
{{ crate::picture_generator::picture_markup_generator::generate_picture_markup(img, 360, 128, alt_text, Some("h-auto mx-auto rounded-sm")).unwrap_or("thumbnail not found".to_string())|safe }}
|
||||
{{ crate::picture_generator::picture_markup_generator::generate_picture_markup(img, 360, 128, alt_text, Some("h-auto mx-auto rounded-xs")).unwrap_or("thumbnail not found".to_string())|safe }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% macro talent_card(svg, heading, description) %}
|
||||
<section class="flex border rounded bg-white p-3">
|
||||
<section class="flex border border-slate-200 rounded-sm bg-white p-3">
|
||||
<aside class="flex justify-center items-center pr-3">
|
||||
<svg role="img" aria-label="{{svg}} icon" aria-hidden="true" class="fill-blue-950 h-12 w-12 md:h-16 md:w-16">
|
||||
<use href="#{{svg}}" />
|
||||
|
@ -1,18 +0,0 @@
|
||||
<section class="flex border rounded bg-white p-3">
|
||||
<aside class="flex justify-center items-center pr-3 shrink-0">
|
||||
{% match workplace.thumbnail %}
|
||||
{% when Some with (source) %}
|
||||
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "Workplace cover", Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
|
||||
<figure class="mx-4 my-2">
|
||||
{{picture|safe}}
|
||||
</figure>
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
</aside>
|
||||
<section>
|
||||
<header>
|
||||
<h3 class="text-lg font-medium mb-1 md:text-2xl">{{workplace.name}}</h3>
|
||||
</header>
|
||||
<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{workplace.description|parse_markdown|safe}}</section>
|
||||
</section>
|
||||
</section>
|
@ -14,6 +14,6 @@
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</symbol><symbol id="linkedin" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/></symbol><symbol id="mail" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></symbol><symbol id="person-chalkboard" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M192 96a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm-8 384V352h16V480c0 17.7 14.3 32 32 32s32-14.3 32-32V192h56 64 16c17.7 0 32-14.3 32-32s-14.3-32-32-32H384V64H576V256H384V224H320v48c0 26.5 21.5 48 48 48H592c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48H368c-26.5 0-48 21.5-48 48v80H243.1 177.1c-33.7 0-64.9 17.7-82.3 46.6l-58.3 97c-9.1 15.1-4.2 34.8 10.9 43.9s34.8 4.2 43.9-10.9L120 256.9V480c0 17.7 14.3 32 32 32s32-14.3 32-32z"/></symbol><symbol id="phone" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M164.9 24.6c-7.7-18.6-28-28.5-47.4-23.2l-88 24C12.1 30.2 0 46 0 64C0 311.4 200.6 512 448 512c18 0 33.8-12.1 38.6-29.5l24-88c5.3-19.4-4.6-39.7-23.2-47.4l-96-40c-16.3-6.8-35.2-2.1-46.3 11.6L304.7 368C234.3 334.7 177.3 277.7 144 207.3L193.3 167c13.7-11.2 18.4-30 11.6-46.3l-40-96z"/></symbol><symbol id="rss" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></symbol><symbol id="tiktok-2" viewBox="0 0 24 24">
|
||||
</symbol><symbol id="linkedin" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/></symbol><symbol id="mail" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/></symbol><symbol id="mastodon" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"/></symbol><symbol id="person-chalkboard" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M192 96a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm-8 384V352h16V480c0 17.7 14.3 32 32 32s32-14.3 32-32V192h56 64 16c17.7 0 32-14.3 32-32s-14.3-32-32-32H384V64H576V256H384V224H320v48c0 26.5 21.5 48 48 48H592c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48H368c-26.5 0-48 21.5-48 48v80H243.1 177.1c-33.7 0-64.9 17.7-82.3 46.6l-58.3 97c-9.1 15.1-4.2 34.8 10.9 43.9s34.8 4.2 43.9-10.9L120 256.9V480c0 17.7 14.3 32 32 32s32-14.3 32-32z"/></symbol><symbol id="phone" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M164.9 24.6c-7.7-18.6-28-28.5-47.4-23.2l-88 24C12.1 30.2 0 46 0 64C0 311.4 200.6 512 448 512c18 0 33.8-12.1 38.6-29.5l24-88c5.3-19.4-4.6-39.7-23.2-47.4l-96-40c-16.3-6.8-35.2-2.1-46.3 11.6L304.7 368C234.3 334.7 177.3 277.7 144 207.3L193.3 167c13.7-11.2 18.4-30 11.6-46.3l-40-96z"/></symbol><symbol id="rss" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM96 136c0-13.3 10.7-24 24-24c137 0 248 111 248 248c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-110.5-89.5-200-200-200c-13.3 0-24-10.7-24-24zm0 96c0-13.3 10.7-24 24-24c83.9 0 152 68.1 152 152c0 13.3-10.7 24-24 24s-24-10.7-24-24c0-57.4-46.6-104-104-104c-13.3 0-24-10.7-24-24zm0 120a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></symbol><symbol id="tiktok-2" viewBox="0 0 24 24">
|
||||
<path d="M16.8217 5.1344C16.0886 4.29394 15.6479 3.19805 15.6479 2H14.7293M16.8217 5.1344C17.4898 5.90063 18.3944 6.45788 19.4245 6.67608C19.7446 6.74574 20.0786 6.78293 20.4266 6.78293V10.2191C18.645 10.2191 16.9932 9.64801 15.6477 8.68211V15.6707C15.6477 19.1627 12.8082 22 9.32386 22C7.50043 22 5.85334 21.2198 4.69806 19.98C3.64486 18.847 2.99994 17.3331 2.99994 15.6707C2.99994 12.2298 5.75592 9.42509 9.17073 9.35079M16.8217 5.1344C16.8039 5.12276 16.7861 5.11101 16.7684 5.09914M6.9855 17.3517C6.64217 16.8781 6.43802 16.2977 6.43802 15.6661C6.43802 14.0734 7.73249 12.7778 9.32394 12.7778C9.62087 12.7778 9.9085 12.8288 10.1776 12.9124V9.40192C9.89921 9.36473 9.61622 9.34149 9.32394 9.34149C9.27287 9.34149 8.86177 9.36884 8.81073 9.36884M14.7244 2H12.2097L12.2051 15.7775C12.1494 17.3192 10.8781 18.5591 9.32386 18.5591C8.35878 18.5591 7.50971 18.0808 6.98079 17.3564" stroke="#000000" stroke-linejoin="round"/>
|
||||
</symbol><symbol id="tiktok" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M448 209.9a210.1 210.1 0 0 1 -122.8-39.3V349.4A162.6 162.6 0 1 1 185 188.3V278.2a74.6 74.6 0 1 0 52.2 71.2V0l88 0a121.2 121.2 0 0 0 1.9 22.2h0A122.2 122.2 0 0 0 381 102.4a121.4 121.4 0 0 0 67 20.1z"/></symbol><symbol id="twitch" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M391.2 103.5H352.5v109.7h38.6zM285 103H246.4V212.8H285zM120.8 0 24.3 91.4V420.6H140.1V512l96.5-91.4h77.3L487.7 256V0zM449.1 237.8l-77.2 73.1H294.6l-67.6 64v-64H140.1V36.6H449.1z"/></symbol><symbol id="twitter" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></symbol><symbol id="youtube" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M549.7 124.1c-6.3-23.7-24.8-42.3-48.3-48.6C458.8 64 288 64 288 64S117.2 64 74.6 75.5c-23.5 6.3-42 24.9-48.3 48.6-11.4 42.9-11.4 132.3-11.4 132.3s0 89.4 11.4 132.3c6.3 23.7 24.8 41.5 48.3 47.8C117.2 448 288 448 288 448s170.8 0 213.4-11.5c23.5-6.3 42-24.2 48.3-47.8 11.4-42.9 11.4-132.3 11.4-132.3s0-89.4-11.4-132.3zm-317.5 213.5V175.2l142.7 81.2-142.7 81.2z"/></symbol></svg>
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
@ -174,7 +174,7 @@
|
||||
|
||||
<ul class="m-6 flex gap-2 flex-wrap justify-center">
|
||||
{% for technology in technology_list %}
|
||||
<li class="p-2 text-pink-900 bg-blue-100 text-sm border rounded border-blue-300 font-mono">
|
||||
<li class="p-2 text-pink-900 bg-blue-100 text-sm border rounded-sm border-blue-300 font-mono">
|
||||
{{technology}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -10,7 +10,7 @@
|
||||
</a>
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
<aside class="flex logo-section flex-grow justify-end content-end">
|
||||
<aside class="flex logo-section grow justify-end content-end">
|
||||
<a class="logo p-3 text-base" href="/">
|
||||
@michalvankodev
|
||||
</a>
|
||||
|
Reference in New Issue
Block a user