40 Commits

Author SHA1 Message Date
134159f79c added logo bg pulsing effect
Some checks failed
test / cargo test (push) Failing after 1m41s
2025-06-15 22:20:00 +02:00
489156fe87 add mastodon 2025-06-15 22:20:00 +02:00
83a24557bd Merge pull request #16 from michalvankodev/renovate/axum-monorepo
Update Rust crate axum to 0.8.0
2025-06-13 20:51:04 +02:00
facb304a52 Merge pull request #19 from michalvankodev/renovate/askama_escape-0.x
Update Rust crate askama_escape to 0.13.0
2025-06-13 20:50:52 +02:00
1e8f48b6fe Merge pull request #18 from michalvankodev/renovate/askama-0.x
Update Rust crate askama to 0.14
2025-06-13 20:50:28 +02:00
1473b676f4 Update Rust crate axum to 0.8.0 2025-06-13 18:50:03 +00:00
74f875460a Update Rust crate askama to 0.14 2025-06-13 18:50:00 +00:00
cd639a830b Merge pull request #20 from michalvankodev/renovate/askama_axum-0.x
Update Rust crate askama_axum to 0.5.0
2025-06-13 20:49:28 +02:00
cde3faa3c6 Update Rust crate askama_axum to 0.5.0 2025-04-18 15:32:49 +00:00
e8f9ecc241 Update Rust crate askama_escape to 0.13.0 2025-03-27 20:15:53 +00:00
1a059a005a Merge pull request #17 from michalvankodev/renovate/pulldown-cmark-0.x
Update Rust crate pulldown-cmark to 0.13
2025-02-12 22:53:34 +01:00
ff087b0577 Update Rust crate pulldown-cmark to 0.13 2025-02-12 18:27:55 +00:00
13820f58eb Recommended articles
Some checks failed
test / cargo test (push) Failing after 59s
2024-11-24 15:38:05 +01:00
f7eb6cc95d Logic for recommended articles 2024-11-24 14:56:54 +01:00
fec60900f5 add images to rss feed
Some checks failed
test / cargo test (push) Failing after 1m8s
2024-11-24 14:06:14 +01:00
96ead1a38f Description in RSS feed 2024-11-24 13:35:23 +01:00
0509565c2b git_afk blog post
Some checks failed
test / cargo test (push) Failing after 1m22s
2024-11-24 13:15:48 +01:00
0b3da60cad featured articles
Some checks failed
test / cargo test (push) Failing after 1m31s
2024-11-11 12:58:54 +01:00
0228429ad0 metaframer article
Some checks failed
test / cargo test (push) Failing after 1m5s
2024-11-08 18:06:13 +01:00
39596cb39c update readme
Some checks failed
test / cargo test (push) Failing after 1m5s
2024-11-08 17:06:04 +01:00
55c8f29b24 Merge pull request #12 from michalvankodev/renovate/tower-http-0.x
Some checks failed
test / cargo test (push) Failing after 1m3s
Update Rust crate tower-http to 0.6.0
2024-11-08 10:37:50 +01:00
289763baec Merge pull request #14 from michalvankodev/renovate/node-22.x
Update Node.js to v22
2024-11-08 10:35:09 +01:00
5338dcd0ba Merge pull request #11 from michalvankodev/renovate/pulldown-cmark-0.x
Some checks failed
test / cargo test (push) Failing after 1m34s
Update Rust crate pulldown-cmark to 0.12
2024-11-08 10:33:37 +01:00
921b2a493f Merge branch 'main' into renovate/pulldown-cmark-0.x 2024-11-08 09:51:31 +01:00
b0e49a904a broadcast hypermedia systems
Some checks failed
test / cargo test (push) Failing after 1m6s
2024-11-06 17:42:09 +01:00
355a4c4d04 post update
Some checks failed
test / cargo test (push) Failing after 1m1s
2024-11-06 10:02:17 +01:00
e44b4a099c site update article
Some checks failed
test / cargo test (push) Failing after 1m27s
2024-11-05 08:39:39 +01:00
e1384d1812 Update Node.js to v22 2024-10-31 18:27:13 +00:00
2ec4ed6c4e animated logo
Some checks failed
test / cargo test (push) Failing after 1m4s
2024-10-31 19:25:53 +01:00
39b976b182 Update Rust crate tower-http to 0.6.0 2024-10-31 09:11:09 +00:00
34b67d9ac1 Update Rust crate pulldown-cmark to 0.12 2024-10-31 09:11:04 +00:00
2ff39c6c1d Merge pull request #10 from michalvankodev/renovate/configure
Configure Renovate
2024-10-31 10:10:45 +01:00
9dca2bfac1 Add renovate.json 2024-10-31 09:06:39 +00:00
8341320b23 perfected the animation
Some checks failed
test / cargo test (push) Failing after 1m16s
2024-10-25 21:58:53 +02:00
85e86b28bb animated logo in portfolio 2024-10-25 20:29:49 +02:00
a2fdec755b portfoliooooo
Some checks failed
test / cargo test (push) Failing after 1m24s
2024-10-23 16:01:48 +02:00
086ec33d7b view transitions with almost no css
Some checks failed
test / cargo test (push) Failing after 1m10s
2024-10-09 11:18:47 +02:00
d9d17bb971 image generation is now always wanted
Some checks failed
test / cargo test (push) Failing after 1m4s
2024-10-07 15:42:54 +02:00
11cc9f6d0a change the whole parsing of markdown functionality 2024-10-07 15:35:22 +02:00
fb6ca6c245 fix not-found
Some checks failed
test / cargo test (push) Failing after 1m9s
2024-10-07 10:17:45 +02:00
66 changed files with 1638 additions and 784 deletions

View File

@ -13,7 +13,7 @@ jobs:
name: release name: release
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: node:18 image: node:22
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable

View File

@ -6,17 +6,17 @@ 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.12", features = ["with-axum", "mime", "mime_guess"] } askama = { version = "0.14", features = ["with-axum", "mime", "mime_guess"] }
askama_axum = "0.4.0" askama_axum = "0.5.0"
axum = "0.7.3" axum = "0.8.0"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
pulldown-cmark = { version = "0.10" } pulldown-cmark = { version = "0.13" }
gray_matter = "0.2.6" gray_matter = "0.2.6"
rss = "2.0.7" rss = "2.0.7"
serde = "1.0.195" serde = "1.0.195"
serde_json = "1.0.111" serde_json = "1.0.111"
tokio = { version = "1.35.1", features = ["full"] } 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" tower-livereload = "0.9.2"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@ -25,6 +25,8 @@ anyhow = "1.0.86"
rayon = "1.10.0" rayon = "1.10.0"
syntect = "5.2.0" syntect = "5.2.0"
indoc = "2.0.5" indoc = "2.0.5"
askama_escape = "0.13.0"
mime_guess = "2.0.5"
[build] [build]
rustflags = ["-Z", "threads=8"] rustflags = ["-Z", "threads=8"]

102
README.md
View File

@ -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 Feel free to use and ammend to code to your needs.
# for Rollup 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.
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
```
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. 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.
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).

View File

@ -1,25 +1,16 @@
--- ---
title: Portfolio - Michal Vanko title: Portfolio - Michal Vanko
work_history_prelude: >-
I've started learning web development when I was 14 years old. My very first
website was a presentation site for my own _Counter-Strike_ clan.
Then I had an opportunity to create a registration system for marathon runners for Europe's oldest marathon event. That basically started off my career as a web developer. I've worked on some projects while I was studying in high school and university. After that, I've started to work full-time as a web developer and gained more experience in developing real-time web applications.
work_history: work_history:
- displayed: true
name: Freelancer
thumbnail: /images/uploads/m-logo.svg
description: Since 2022, I have worked as a self-employed freelancer. I provide
consultations and build safe and robust web applications for clients.
- description: >- - description: >-
_sudolabs_ is a company focused on building products and software *sudolabs* is a company focused on building products and software
solutions for start-ups & other clients. solutions for start-ups & other clients.
The company has its own product lab focused on product research and product discovery and an engineering lab focused on the development of high-quality software solutions. I've worked there as a **tech lead and an Engineering manager**.
At _sudolabs_, I've managed to proceed from senior software engineer to **tech lead and an Engineering manager**.
I am part of one of the most successful projects at _sudolabs_, _The Expert_. I am responsible for the architecture and quality of the proposed feature solutions and for the development processes used.
As an Engineering manager, I try to **represent people in my team** and help them **achieve the best of their potential**.
I've started to work for _sudolabs_ in October 2019.
name: sudolabs s.r.o. name: sudolabs s.r.o.
address: address:
name: Sudo Labs. s.r.o. name: Sudo Labs. s.r.o.
@ -28,6 +19,7 @@ work_history:
city: Košice city: Košice
country: Slovakia country: Slovakia
displayed: true displayed: true
thumbnail: /images/uploads/6708e31603754bd2b4c6ed11_sudo_logo.svg
- description: >- - description: >-
_**Croptech**_ is a start-up company located in Košice focused on smart _**Croptech**_ is a start-up company located in Košice focused on smart
automation of hydroponic systems. Founded in 2014 they've won automation of hydroponic systems. Founded in 2014 they've won
@ -44,14 +36,9 @@ work_history:
zipcode: 040 18 zipcode: 040 18
city: Košice city: Košice
country: Slovakia country: Slovakia
- description: >- - description: I worked in *localhost* as a **Senior front end developer**. I was
_localhost.company_ was a small company that had around 40 developers focused on delivering a high-quality product and teaching and helping
while most of them were students. I worked in _localhost_ as a **Senior other colleagues grow in expertise.
front end developer**. I was focused on delivering a high-quality product
and to teach and helping other colleagues grow in expertise.
I've started to work there in October 2018 and left after a year.
name: localhost.company s.r.o. name: localhost.company s.r.o.
displayed: true displayed: true
address: address:
@ -60,15 +47,9 @@ work_history:
zipcode: 040 01 zipcode: 040 01
city: Košice city: Košice
country: Slovakia country: Slovakia
- description: > thumbnail: /images/uploads/LH-symbol-with-borders-WHITE-RED-BGb.png
I worked in _Ness_ as a Senior front-end developer. In _Ness_, I've been - description: I worked in *Ness* as a Senior front-end developer. In *Ness*, I've
able to work for different clients on various projects. been able to work for different clients on various projects for 6 years.
I joined Ness as a Junior front-end developer and I've been able to quickly proceed to Senior level with my engagement.
I started to work in March 2013 and left in October 2018.
name: Ness KE s.r.o. name: Ness KE s.r.o.
displayed: true displayed: true
address: address:
@ -77,15 +58,9 @@ work_history:
zipcode: 040 11 zipcode: 040 11
city: Košice city: Košice
country: Slovakia country: Slovakia
- description: >- thumbnail: /images/uploads/nesskosicelogo.svg
I worked in _eSOLUTIONS_ as a **web developer** part-time while I was - description: I worked for *eSOLUTIONS* as a part-time **web developer** while I
studying in high school and at University. was in high school and at University.
I was building front-end and back-end services for websites and web applications. Mostly using _PHP_, _MySQL_, _JavaScript_, _HTML_, _CSS_.
I started to work in 2007 and stopped in 2012.
name: eSOLUTIONS s.r.o. name: eSOLUTIONS s.r.o.
displayed: true displayed: true
address: address:
@ -94,285 +69,30 @@ work_history:
zipcode: 040 01 zipcode: 040 01
city: Košice city: Košice
country: Slovakia country: Slovakia
projects: thumbnail: /images/uploads/esolutions_logo.svg
- description: >-
_The Expert_ is a digital platform that connects clients to interior
designers around the world. For experts, it allows **managing** their
**portfolio and profile** and **schedule** in which they are open for
**consultations**. Clients are able to view their profiles and book
consultations.
The project is **expanding with an e-commerce platform** that should enable these experts to recommend and sell high-value goods.
We have built this platform from scratch as the only technical team. We also managed to design the product itself in-house.
I am part of the engineering team. I work as an **Engineering manager**. I am also **responsible for the technical architecture** and **development processes** that are being used to deliver new features.
_The Expert_ has been **featured in** numerous **worldwide media** such as [_The New York Times_](https://www.nytimes.com/2021/10/19/business/home-design-zoom.html?smid=url-share), [_Wall Street Journal_](https://www.wsj.com/articles/how-you-can-afford-to-hire-your-dream-interior-designer-11619627877), [_Forbes_](https://www.forbes.com/sites/emmareynolds/2021/02/08/introducing-the-expert-a-new-digital-platform-for-interior-design-founded-by-designer-jake-arnold/?sh=33b274934471), [_TechCrunch_](https://techcrunch.com/2021/04/19/gwyneth-paltrow-invests-in-the-expert-a-video-marketplace-for-high-end-interior-designers/?guccounter=2), and many more.
displayed: true
name: The Expert
image:
source: /images/uploads/the-expert-logo.svg
image_description: The Expert logo
- description: >-
*Panoramic* is a company focused on building a web application for data
scientists to be able to create and share models and graphs in between
each other.
I've been working as a **team lead** of a small team that was focused on building a **rich-text collaboration** feature into the web application.
The application was built on top of modern web technologies such as **React, node.js with GraphQL, and TypeScript**. We have successfully built this feature by integrating *[Mattermost](https://mattermost.com/)* into the existing back-end. After that, we have been helping another team by enhancing existing features for importing data sources.
displayed: true
name: Panoramic
- description: >-
_Manualogic_ is a **single-page application** for product manual creators.
It contains **custom web editor** and management system of **translatable
pages, books** and **products.** Its main goal is to enable customers to
get manuals of their products in digital form.
It's built on top of modern web technologies such as **Angular**, **RxJS**, and the content is synchronized through **Websockets**. The back-end part is built on top of **Spring framework**.
The team consisted of 3-4 front-end developers, 2-3 back-end developers, and 2 testers.
displayed: true
name: Manualogic
- description: >-
_**responzIO**_ is smart, easy to use monitoring and automation system.
The ultimate tool for various applications such as hydroponics, aquariums
and gardens.
I worked on _responzIO_ as architect and full-stack developer. I've designed complete software solution for an already created platform and a series of sensors, sockets and other hardware. Designed solution consist of varios parts: Deployment, Microservices, Stateful core back-end application, progressive web application, auto-update over web...
This project is built with modern web technologies including: **CycleJS**, **Reactive Streams**, **D3**, **Jest**, **Webpack**.
displayed: true
image:
image_description: ' responzIO main controller'
source: /images/uploads/responzio.png
name: responzIO
- description: >-
_Signal Hub_ is an end-to-end **Big Data analytics platform** for large
enterprises. It accelerates the process of extracting insights and
intelligence from large volumes of data, including data of different types
and in different formats.
I worked on _Signal Hub_ as a front-end developer. Working on the part of the platform called _Workbench_. _Workbench_ is an **IDE for data analyst**. It allows to create and tweak various models and transformations which are then deployed to other parts of the platform. Users are able to create workflows and see the results of his work with pleasant visuals.
This project is built with modern web technologies including: **AngularJS**, **React**, **D3**, **Highcharts**, **Jest**, **Webpack**.
I was part of a large distributed team of ~20 developers & ~6 testers which were working on the whole platform.
displayed: true
name: Signal Hub Manager
- description: >-
_SHIP_ is a web application for **editors** who actively **track trades
offers and bids** on the commodity market.
It was built for a price reporting company _S&P Global Platts_. This company is a provider of energy and commodities information and a source of benchmark price assessments in the _physical energy markets_.
_SHIP_ was suited for a very easy and quick workflow for editors.
Team consisted of 3 front-end and 3 back-end developers. I was a **leader of the front-end** part of the project.
The application was built with **AngularJS and Redux**. The challenging part was working with **Cassandra database**.
displayed: true
name: SHIP (Structured heard input process)
- description: >-
[www.caplin.com/business/fx-sales.com](http://www.caplin.com/business/fx-sales)
The main function of _Caplin FX Sales_ is to allow sales users to <string>trade on behalf of their clients</string>. This needs to be an efficient workflow providing all the relevant information to the sales user.
Main technologies used: **Websockets**, Custom Front-end JavaScript MVVM Framework (based on Knockout, BRJS)
We worked on this project in a team of 6 developers and 3 testers
displayed: true
name: FX Salestrader
- description: >-
_Skosy_ is a web application which purpose is to **automate writing of
integration tests** for web sites.
I worked on a _proof of concept_ front-end portion of this application, which was able to create a list of steps for testing.
I worked on this project in a team of 2 front-end developers.
displayed: false
name: Skosy
- description: >-
[www.livesport.tv](http://www.livesport.tv)
_Livesport.tv_ is a network of premium online sports channels, featuring all the top sports competitions from around the world.
I worked on this project as a front-end developer. I maintained all livesport channels and I worked on new features and enhancements of websites.
Project was built on top of **Freemarker** templating language.
The team consisted of 2 developers and 1 tester.
displayed: true
name: Livesport.tv
- description: >-
_beIN Sports_ is a global network of sports channels jointly owned and
operated by _Qatari Sports Investments_, an affiliate of _Al Jazeera Media
Networks_
I worked on this project as a front-end developer. I maintained official websites of beIN SPORTS France and USA ([www.beinsports.fr](http://www.beinsports.fr), [www.beinsports.tv](http://www.beinsports.tv)).
Project was built on top of **Freemarker** templating language.
I worked on this project in a team consisting of 7 members.
displayed: true
name: beIN SPORTS France & beIN SPORTS USA
- description: >-
I worked on this project as a front-end developer. I maintained official
websites of all Norweigian Football teams - NTF Network
([www.toppfotball.no](http://www.toppfotball.no))
Project was built on top of **Freemarker** templating language.
I worked on this project in a team consisting of 4 members.
displayed: false
name: Norsk Toppfotball
- description: >-
[www.kosicemarathon.com](http://www.kosicemarathon.com/)
_Košice Peace Marathon_ is the oldest marathon in Europe and the third-oldest in the world.
I worked on this project as a front-end and an back-end developer. I've built a **custom CMS** for website and for registration of participants. Also created an application for actual event for signing participants.
The application was able to create a start list of all participants and provide various statistics about them.
I've built this project alone and then one maintainer joined me.
displayed: true
name: Košice Peace Marathon
- description: >-
[www.ckvive.sk](http://www.ckvive.sk)
_CK Vive_ is a travel agency.
I worked on this project as a front-end and an back-end developer. I've created new **custom CMS** for adding destinations and apartments into offering.
I've built this project on top of **PHP**, **MySQL** and **JavaScript**.
I worked on this project alone.
displayed: false
name: CK Vive
presentations:
- displayed: true
link: https://michalvankodev.github.io/unstoppable-growth-of-frontend-frameworks/
name: Unstoppable growth of front-end frameworks
description: >
A simple summary of the web front-end evolution, which will try to
describe how and why tools improve & why there is still something to
explore.
- displayed: true
name: WebAssembly
link: https://michalvankodev.github.io/presentation-webassembly/#/
description: A presentation about what WebAssembly is about and how it might
affect the future of the world.
- displayed: true
name: Spreading the web
description:
A presentation about the rising number of use cases for utilizing
web technologies outside of the web platform such as native mobile
applications and robotics.
link: https://michalvankodev.github.io/spreading-the-web/#/
- displayed: true
name: Docker
link: http://michalvankodev.github.io/dockerpresentation/#/
description:
An introduction to Docker containerization technology and how it
differs from virtualization.
education: education:
- description: |- - description: |-
Faculty of Electrical Engineering and Informatics - Intelligent Systems Faculty of Electrical Engineering and Informatics - Intelligent Systems
_Artificial Intelligence, Neural Networks, Fuzzy Systems_ _Artificial Intelligence, Neural Networks, Fuzzy Systems_
Dates: 1\. 9\. 2010 - 20\. 6\. 2013 Year of 2013
Title of qualification awarded: Bachelor (Bc.) Title of qualification: Bachelor (Bc.)
displayed: true displayed: true
name: Technická univerzita Košice name: Technická univerzita Košice
- description: >- thumbnail: /images/uploads/tuke_znak_b_cmyk.png
- description: |-
_SPŠ Elektrotechnická, Komenského 22, Košice_ _SPŠ Elektrotechnická, Komenského 22, Košice_
Electrotechnics, Telecommunication Electrotechnics, Telecommunication
Year of 2010
Dates: 1\. 9\. 2005 - 1\. 7\. 2010 Full secondary school education with leaving examination
Title of qualification awarded: Full secondary school education with leaving examination
displayed: true displayed: true
name: Secondary school of Electrical Engineering in Košice name: Secondary school of Electrical Engineering in Košice
thumbnail: /images/uploads/logo_spse_far.png
--- ---
As a passionate software developer with over half my life spent coding professionally, I've developed a strong affinity for building **highly scalable** and **proficient systems**. With extensive experience in **web application development**, I've honed my skills across various technological stacks, leveraging expertise to drive results. When not crafting complex solutions, you can find me indulging in **game development** - a creative outlet that allows me to bring fun and innovation to the table. As a **tech lead and mentor**, I thrive in environments where **quality software, tests, and collaboration** come together to **create innovative solutions**.
## Personal Information In addition to my technical expertise, I bring a wealth of **experience as an Engineering manager**, where I've honed my **leadership skills** and developed a talent for **mentoring teams**. When not coding or leading teams, you can find me exploring the world through **streaming, building an online community** around programming, **making music**, and spending quality time with my family as a **proud father**. I'm also an avid board game enthusiast! As a thought leader, I aim to leverage my diverse experience and skills to **provide exceptional service** to my customers and make a positive impact on the tech industry.
I was born in Košice, Slovakia and I still live here. I like to drink tea and cook my own recipes.
### Hobbies
I enjoy playing basketball with my friends. I also like to play other team sports like football and hockey. I also play squash and table tennis. Once I've won a competition in squash at my university. During summer, I love to go water skiing or swimming in a nearby lake.
I am very passionate about music. I've also tried to use some digital workstations to compose my own music.
I enjoy playing board games with my friends and family.
### Interests
I like to explore new technologies and _Open-source_ software.
I am interested in modern software architecture and _reactive programming_.
I've attended several **tech conferences and hackathons**, where I can meet interesting people and explore fascinating ideas.
I've given presentations on various topics related to _web development_. You can [take a look at some of them here](#presentations).
I enjoy **teaching and explaining** how technologies and techniques work to my colleagues or students for their better understanding.
I take advantage of **test-driven development**.
## Skills
Slovak is my mother tongue and I've learned English as my second language. I speak English on an advanced level.
I'm an experienced _Linux Desktop_ user. I prefer to use open-source libraries and technologies while I develop solutions.
I'm in good command of Office Tools and I have experience with image manipulation programs like _GIMP_ and _Inkscape_.
I can also compose music and sounds on _digital audio workstation_.
I'm passionate about _software architecture_. My goal is not only to be able to design a suitable solution for any kind of product but also to be able to architect the processes behind building a team. From small _presentation sites_, _IOT devices_, to large _enterprise applications running on cloud_.
I practice **public speaking** and I am not afraid to share my knowledge and passion about technology.
I'm an advanced user of source code management tools _git_ and _svn_.
I've got a driving license for category B 🚗.

View File

@ -3,7 +3,6 @@ layout: blog
title: Introduction to JavaScript Application testing title: Introduction to JavaScript Application testing
segments: segments:
- blog - blog
- featured
published: true published: true
date: 2021-05-07T14:44:57.102Z # update date accordingly date: 2021-05-07T14:44:57.102Z # update date accordingly
tags: tags:

View File

@ -1,7 +1,6 @@
--- ---
segments: segments:
- blog - blog
- featured
notes: "" notes: ""
layout: blog layout: blog
title: "Our attempt at Rusty game jam - Weekly #25-2022" title: "Our attempt at Rusty game jam - Weekly #25-2022"

View File

@ -0,0 +1,76 @@
---
title: This site has been rewritten again
segments:
- blog
- featured
published: true
date: 2024-11-05T11:26:00.000Z
thumbnail: /images/uploads/m-logo.svg
tags:
- News
- Development
- Rust
notes: Bu
---
Hey world,
After a few months of work, I can finally present to you a new style for this site. The style isn't the only thing that changed.
I've **completely rewritten** how this site is produced.
## Motivation
With the front-end tech world spinning too fast I've noticed this cool little library called *[HTMX](https://htmx.org/)* that takes front-end development back 10 years and provides a completely different look at how web development could've been done.
I've also done some experiments with [Qwik](https://qwik.dev/) before, but I'm glad that I've abandoned this route as I would like to write less and less *TypeScript* `||` *Javascript* in the future.
After doing last year's [Advent of Code](https://adventofcode.com/) in *OCaml* I've figured that I'd like to use tools that I think make me productive and also provide me help on the path of producing the most safe, predictable and performant code.
That's the reason **I've chosen *Rust*** for this site and most of my side projects in the last couple of years.
## What has changed
### Tech stack
I've mentioned *HTMX*, but it only inspired me and I took it a few steps back and I haven't opted for any *JavaScript* included by default. You can find some *JavaScript* on this site but it is only inlined without any build step. However, there still has to be a build step. Instead of setting up *[Vite](https://vite.dev/)* for 5th time. I opted for something classic but still new. I've just found the [just](https://github.com/casey/just) command runner. It is very similar to how many *Makefiles* look, but the API is modern and **written in Rust BTW**. So if any developer wants to look at what commands are being used during the development or deployment, they are all located in [`.justfile`](https://github.com/michalvankodev/michalvankodev-site/blob/main/justfile). All commands are just `bash` commands and some `bash` scripts with few instructions for `just` to combine commands by their dependencies.
### Templates
During development, the site is being hosted with [axum Rust web framework](https://github.com/tokio-rs/axum). I want to write my templates in HTML so they can be as easily reused in any other web project with any tech stack. For Rust, there is a **type-safe** Jinja like template parser [Askama](https://github.com/djc/askama) and even though I'm not very confident whether I would use it again, I can still easily migrate to any other template parser there is.
### Styling
I've completely redesigned the site. Dodging *TypeScript-based* solutions for <abbr title="Cascade style sheets">CSS</abbr>. Opting for [tailwind](https://tailwindcss.com/) running *just* in the background. The experience with *tailwind* is something that I've never felt previously with any other <abbr title="Cascade style sheets">CSS</abbr> framework before. I'm satisfied with it although it has some downsides, **the productivity hasn't been matched**.
## What stayed the same
### All the content
I haven't even moved the files to a different folder. I've just slightly adjusted the models in the [DecapCMS](https://decapcms.org/) config for [/showcase](/showcase) page. DecapCMS is pretty much just the same Netlify CMS that it has been before the rebranding.
I'd like to recommend it as *THE CMS*, but I can't. I like the way how the content is managed, I also like *git-based* workflow for managing content. However, there are many struggles to set it up for use with clients that are not programmers. With more changes to the models you also start to **miss the capability of running migrations** on all files/records. Everyone would be better off just hosting *[Strapi](https://strapi.io/)*. The ecosystem has started to get healthy. Setting up a custom media library is also a thing that has to be considered.
### Media library hosting
[Netlify has deprecated](https://answers.netlify.com/t/large-media-feature-deprecated-but-not-removed/100804) its [Large Media](https://docs.netlify.com/git/large-media/setup/) service. I don't mind tracking media with `git` LFS plugin. What I do mind is where I store these assets. \[Github's] free quota is pretty low considering sharing photo galleries. For now, I've moved all assets on my *"powered by"* [selfhosted](https://awesome-selfhosted.net/index.html) server, running my own [Gitea](https://gitea.katelyn.michalvanko.dev/) instance.
### Static site generation
The site is still distributed as <abbr title="Static site generation">SSG</abbr> HTML files. You could argue that the logic for generating every page is just like any other <abbr title="Server Side Rendered">SSR</abbr> website.
For <abbr title="Server side generation">SSG</abbr> I've come up with a `wget` command that downloads all the necessary dependencies for all pages. It is capable of recursively crawling the whole site following all links. It was pretty hard to come up with the correct set of parameters for the `wget` command to be able to produce the same routing capabilities as with the `SSR` running server used during the development. Here I can **praise all the generative AI tools**. You could find multiple prompts asking for an explanation of all the options for `wget` and how I should use them for the desired output in my history.
The final `wget` command for generating this site looks like this:
```sh
wget --no-convert-links -r -p -E -P dist --no-host-directories localhost:3080
```
You can also [prompt for the explanation](https://lmgpthat.com/render.html?search=wget%20--no-convert-links%20-r%20-p%20-E%20-P%20dist%20--no-host-directories%20localhost%3A3080).
## Aim for future
I still have to **write articles**.
Many many articles that I haven't yet written down. I did some but haven't published them, and probably I never will. But so many things happened in the meantime. How do you call an update that is old? *"Outofdate"*?
It would also be handy to have some article recommendations when you finish reading this article, right?
I am still gathering feedback for the new design. I am considering many of the suggestions that I get. Our [Discord server](https://discord.gg/2cGg7kwZEh) is getting warm.
I was also thinking about what would I use for CMS and having a *[SQLite](https://www.sqlite.org/docs.html)* database saved in the repository, it would still count as **git-based management**.
*'Let your ambition carry you!"*

View File

@ -0,0 +1,22 @@
---
title: Renaissance of Hypermedia Systems
segments:
- broadcasts
- featured
published: true
date: 2024-11-06T16:20:00.000Z
thumbnail: /images/uploads/screenshot-from-2024-08-06-19-01-03.png
tags:
- Development
- HTMX
- Hyperview
---
My goal for this year was to make a presentation. I've told this to my friend Lukáš Orgován and he offered me an opportunity to come present at their workplace with him. This was a great event and I'm so glad I could attend.
I've chosen a topic very close to my heart and wanted to show a different approach to building mobile and web applications.
The presentation is in Slovak with English subtitles.
<div class="video-embed">
<iframe height="100%" width="100%" src="https://www.youtube.com/embed/hn7rR9wIfIY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>

View 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?

View 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*.

View File

@ -6,7 +6,7 @@ tailwind:
# svg sprite creation # svg sprite creation
svgstore: svgstore:
npx svgstore -o templates/icons/sprite.svg src/svg/**.svg npx svgstore -o templates/icons/sprite.svg static/svg/input/*.svg
server_dev: server_dev:
cargo watch -x run cargo watch -x run
@ -52,7 +52,8 @@ clean:
# SSG # SSG
ssg: ssg:
- wget --no-convert-links -r -p -E -P dist --no-host-directories 127.0.0.1:{{port}} - wget --no-convert-links -r -p -E -P dist --no-host-directories 127.0.0.1:{{port}}
- wget --no-convert-links -p -E -P dist --no-host-directories 127.0.0.1:{{port}}/not-found - wget --no-convert-links --content-on-error -p -E -P dist --no-host-directories 127.0.0.1:{{port}}/not-found
- wget --no-convert-links -p -E -P dist --no-host-directories 127.0.0.1:{{port}}/showcase/m-logo-svg
find generated_images/ -name "*_og*" -exec cp --parents {} dist/ \; find generated_images/ -name "*_og*" -exec cp --parents {} dist/ \;
# Preview server # Preview server

6
renovate.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

View File

@ -1,9 +1,10 @@
use axum::http::{header, StatusCode}; use axum::http::{header, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use chrono::Utc; 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::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; use crate::post_utils::post_listing::get_post_list;
pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> { pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
@ -25,8 +26,30 @@ pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
ItemBuilder::default() ItemBuilder::default()
.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)))
// TODO Description should be just a preview .description({
.description(None) let truncated =
truncate_md(&post.body, 2).unwrap_or("Can't parse post body".to_string());
let parsed_md = parse_markdown(&truncated)
.unwrap_or("Can't process truncated post body".to_string());
Some(parsed_md)
})
.content({
let parsed_md = parse_markdown(&post.body)
.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( .guid(Some(
GuidBuilder::default() GuidBuilder::default()
.value(format!("https://michalvanko.dev/blog/{}", post.slug)) .value(format!("https://michalvanko.dev/blog/{}", post.slug))

View File

@ -1,20 +0,0 @@
use chrono::{DateTime, Utc};
use tracing::debug;
// This filter does not have extra arguments
pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> {
let formatted = format!("{}", date_time.format("%e %B %Y"));
Ok(formatted)
}
// This filter does not have extra arguments
pub fn description_filter(body: &str) -> ::askama::Result<String> {
let description = body
.lines()
.filter(|line| line.starts_with("<p>"))
.take(2)
.collect::<Vec<&str>>()
.join("\n");
debug!(description);
Ok(description)
}

170
src/filters/markdown.rs Normal file
View File

@ -0,0 +1,170 @@
use std::path::Path;
use image::image_dimensions;
use indoc::formatdoc;
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
use syntect::{highlighting::ThemeSet, html::highlighted_html_for_string, parsing::SyntaxSet};
use tracing::{debug, error};
use crate::picture_generator::{
picture_markup_generator::generate_picture_markup, resolutions::get_max_resolution,
};
pub const MAX_BLOG_IMAGE_RESOLUTION: (u32, u32) = (1280, 860);
enum TextKind {
Text,
Heading(Option<String>),
Code(String),
}
// pub fn parse_markdown(markdown: &str) -> ::askama::Result<String>
pub fn parse_markdown(markdown: &str) -> ::askama::Result<String> {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_SMART_PUNCTUATION);
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
let mut text_kind = TextKind::Text;
let syntax_set = SyntaxSet::load_defaults_newlines();
let theme_set = ThemeSet::load_defaults();
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 {
/*
Parsing images considers `alt` attribute as inner `Text` event
Therefore the `[alt]` is rendered in html as subtitle
and the `[](url "title")` `title` is rendered as `alt` attribute
*/
Event::Start(Tag::Image {
link_type: _,
dest_url,
title,
id: _,
}) => {
if !dest_url.starts_with("/") {
return Event::Html(
formatdoc!(
r#"<img
alt="{title}"
src="{dest_url}"
/>"#
)
.into(),
);
}
let dev_only_img_path =
Path::new("static/").join(dest_url.strip_prefix("/").unwrap_or(&dest_url));
let img_dimensions = image_dimensions(&dev_only_img_path).unwrap();
let (max_width, max_height) = get_max_resolution(
img_dimensions,
MAX_BLOG_IMAGE_RESOLUTION.0,
MAX_BLOG_IMAGE_RESOLUTION.1,
);
// Place image into the content with scaled reso to a boundary
let picture_markup = generate_picture_markup(
&dest_url, max_width, max_height, &title, None,
)
.unwrap_or(formatdoc!(
r#"
<img
alt="{alt}"
src="{src}"
/>"#,
alt = title,
src = dest_url,
));
Event::Html(
formatdoc!(
r#"<figure>
{picture_markup}
<figcaption>
"#,
)
.into(),
)
}
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => {
text_kind = TextKind::Code(lang.to_string());
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang)))
}
Event::Text(text) => match &text_kind {
TextKind::Code(lang) => {
// TODO Check https://github.com/trishume/syntect/pull/535 for typescript support
let lang = if ["ts".to_string(), "typescript".to_string()].contains(lang) {
"javascript"
} else {
lang
};
let syntax_reference = syntax_set
.find_syntax_by_token(lang)
.unwrap_or(syntax_set.find_syntax_plain_text());
let highlighted =
highlighted_html_for_string(&text, &syntax_set, syntax_reference, theme)
.unwrap();
Event::Html(highlighted.into())
}
TextKind::Heading(provided_id) => {
let heading_id = provided_id.clone().unwrap_or({
text.to_lowercase()
.replace(|c: char| !c.is_alphanumeric(), "-")
});
debug!("heading_id: {}", heading_id.clone());
match heading_ended {
None => {
error!("Heading should have set state");
panic!("Heading should have set state");
}
Some(true) => Event::Html(text),
Some(false) => {
heading_ended = Some(true);
Event::Html(
formatdoc!(
r##"id="{heading_id}">
{text}"##
)
.into(),
)
}
}
}
_ => Event::Text(text),
},
Event::Start(Tag::Heading {
level,
id,
classes: _,
attrs: _,
}) => {
let id_str = id.map(|id| id.to_string());
debug!("heading_start: {:?}, level: {}", &id_str, level);
text_kind = TextKind::Heading(id_str);
heading_ended = Some(false);
Event::Html(format!("<{level} ").into())
}
Event::Start(_) => event,
Event::End(TagEnd::Image) => Event::Html("</figcaption></figure>".into()),
Event::End(TagEnd::CodeBlock) => {
text_kind = TextKind::Text;
Event::End(TagEnd::CodeBlock)
}
Event::End(TagEnd::Heading(heading_level)) => {
text_kind = TextKind::Text;
heading_ended = None;
Event::End(TagEnd::Heading(heading_level))
}
_ => event,
});
// Write to String buffer
let mut html = String::new();
pulldown_cmark::html::push_html(&mut html, parser);
Ok(html)
}

6
src/filters/mod.rs Normal file
View File

@ -0,0 +1,6 @@
mod markdown;
mod pretty_date;
mod truncate_md;
pub use markdown::parse_markdown;
pub use pretty_date::pretty_date;
pub use truncate_md::truncate_md;

View File

@ -0,0 +1,7 @@
use chrono::{DateTime, Utc};
// This filter does not have extra arguments
pub fn pretty_date(date_time: &DateTime<Utc>) -> ::askama::Result<String> {
let formatted = format!("{}", date_time.format("%e %B %Y"));
Ok(formatted)
}

View File

@ -0,0 +1,18 @@
// This filter does not have extra arguments
const FORBIDDEN_LINES: [&str; 5] = [" ", "#", "-", "!", "<"];
pub fn truncate_md(body: &str, rows: usize) -> ::askama::Result<String> {
let description = body
.lines()
.filter(|line| {
!FORBIDDEN_LINES
.iter()
.any(|forbidden| line.starts_with(forbidden))
&& !line.is_empty()
})
.take(rows)
.collect::<Vec<&str>>()
.join("\n");
Ok(description)
}

View File

@ -38,6 +38,7 @@ async fn main() {
.nest_service("/egg-fetcher", ServeDir::new("static/egg-fetcher")) .nest_service("/egg-fetcher", ServeDir::new("static/egg-fetcher"))
.nest_service("/svg", ServeDir::new("static/svg")) .nest_service("/svg", ServeDir::new("static/svg"))
.nest_service("/config.yml", ServeDir::new("static/resources/config.yml")) // Decap CMS config .nest_service("/config.yml", ServeDir::new("static/resources/config.yml")) // Decap CMS config
.nest_service("/resources", ServeDir::new("static/resources"))
.nest_service("/robots.txt", ServeDir::new("robots.txt")); .nest_service("/robots.txt", ServeDir::new("robots.txt"));
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -55,6 +56,5 @@ async fn main() {
// - fotos // - fotos
// THINK deploy to alula? rather then katelyn? can be change whenever // THINK deploy to alula? rather then katelyn? can be change whenever
// //
// TODO 404 page
// TODO view page transitions
// TODO cookbook // TODO cookbook
// TODO remove m-logo-svg from justfile and mention it in some article!!!

View File

@ -0,0 +1,10 @@
use askama::Template;
use axum::http::StatusCode;
#[derive(Template)]
#[template(path = "assets/animated_logo.html")]
pub struct AnimatedLogoTemplate {}
pub async fn render_animated_logo() -> Result<AnimatedLogoTemplate, StatusCode> {
Ok(AnimatedLogoTemplate {})
}

View File

@ -3,7 +3,9 @@ use axum::extract::OriginalUri;
use axum::{extract::Path, http::StatusCode}; use axum::{extract::Path, http::StatusCode};
use chrono::{DateTime, Utc}; 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::{ use crate::{
blog_posts::blog_post_model::BlogPostMetadata, components::site_header::Link, filters, blog_posts::blog_post_model::BlogPostMetadata, components::site_header::Link, filters,
post_utils::post_parser::parse_post, post_utils::post_parser::parse_post,
@ -18,10 +20,11 @@ pub struct BlogPostTemplate {
pub body: String, pub body: String,
pub date: DateTime<Utc>, pub date: DateTime<Utc>,
pub tags: Vec<String>, pub tags: Vec<String>,
pub segment: String, pub segment: Segment,
pub header_props: HeaderProps, pub header_props: HeaderProps,
pub slug: String, pub slug: String,
pub thumbnail: Option<String>, pub thumbnail: Option<String>,
pub recommended_posts: Vec<ParseResult<BlogPostMetadata>>,
} }
pub async fn render_blog_post( pub async fn render_blog_post(
@ -29,22 +32,27 @@ pub async fn render_blog_post(
OriginalUri(original_uri): OriginalUri, OriginalUri(original_uri): OriginalUri,
) -> Result<BlogPostTemplate, StatusCode> { ) -> Result<BlogPostTemplate, StatusCode> {
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id); let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
let parse_post = parse_post::<BlogPostMetadata>(&path, true); let post = parse_post::<BlogPostMetadata>(&path).await?;
let parsed = parse_post.await?;
let segment = if original_uri.to_string().starts_with("/blog") { let segment = if original_uri.to_string().starts_with("/blog") {
"blog" Segment::Blog
} else if original_uri.to_string().starts_with("/broadcasts") { } else if original_uri.to_string().starts_with("/broadcasts") {
"broadcasts" Segment::Broadcasts
} else { } 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 { let header_props = match segment {
"blog" => HeaderProps::with_back_link(Link { Segment::Blog => HeaderProps::with_back_link(Link {
href: "/blog".to_string(), href: "/blog".to_string(),
label: "All posts".to_string(), label: "All posts".to_string(),
}), }),
"broadcasts" => HeaderProps::with_back_link(Link { Segment::Broadcasts => HeaderProps::with_back_link(Link {
href: "/broadcasts".to_string(), href: "/broadcasts".to_string(),
label: "All broadcasts".to_string(), label: "All broadcasts".to_string(),
}), }),
@ -52,13 +60,40 @@ pub async fn render_blog_post(
}; };
Ok(BlogPostTemplate { Ok(BlogPostTemplate {
title: parsed.metadata.title, title: post.metadata.title,
date: parsed.metadata.date, date: post.metadata.date,
tags: parsed.metadata.tags, tags: post.metadata.tags,
body: parsed.body, body: post.body,
slug: parsed.slug, slug: post.slug,
segment: segment.to_string(), segment,
thumbnail: parsed.metadata.thumbnail, thumbnail: post.metadata.thumbnail,
header_props, header_props,
recommended_posts,
}) })
} }
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)
}

View File

@ -21,7 +21,7 @@ pub struct ContactPageTemplate {
pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> { pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
let links = vec![ let links = vec![
ContactLink { ContactLink {
href: "mailto: michalvankosk@gmail.com".to_string(), href: "mailto:michalvankosk@gmail.com".to_string(),
label: "michalvankosk@gmail.com".to_string(), label: "michalvankosk@gmail.com".to_string(),
title: "E-mail address".to_string(), title: "E-mail address".to_string(),
svg: "mail".to_string(), svg: "mail".to_string(),
@ -44,6 +44,12 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
title: "YouTube channel".to_string(), title: "YouTube channel".to_string(),
svg: "youtube".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 { ContactLink {
href: "https://instagram.com/michalvankodev".to_string(), href: "https://instagram.com/michalvankodev".to_string(),
label: "Instagram".to_string(), label: "Instagram".to_string(),
@ -51,7 +57,7 @@ pub async fn render_contact() -> Result<ContactPageTemplate, StatusCode> {
svg: "instagram".to_string(), svg: "instagram".to_string(),
}, },
ContactLink { ContactLink {
href: "https://instagram.com/michalvankodev".to_string(), href: "https://github.com/michalvankodev".to_string(),
label: "GitHub".to_string(), label: "GitHub".to_string(),
title: "Github profile".to_string(), title: "Github profile".to_string(),
svg: "github".to_string(), svg: "github".to_string(),

View File

@ -1,10 +1,12 @@
pub mod admin; pub mod admin;
pub mod animated_logo;
pub mod blog_post_list; pub mod blog_post_list;
pub mod blog_post_page; pub mod blog_post_page;
pub mod broadcast_list; pub mod broadcast_list;
pub mod contact; pub mod contact;
pub mod index; pub mod index;
pub mod not_found; pub mod not_found;
pub mod portfolio;
pub mod post_list; pub mod post_list;
pub mod project_list; pub mod project_list;
pub mod showcase; pub mod showcase;

139
src/pages/portfolio.rs Normal file
View File

@ -0,0 +1,139 @@
use askama::Template;
use axum::http::StatusCode;
use serde::Deserialize;
use crate::{
components::site_header::HeaderProps,
filters,
post_utils::{
post_listing::get_post_list,
post_parser::{parse_post, ParseResult},
},
projects::project_model::ProjectMetadata,
};
use super::contact::ContactLink;
#[derive(Deserialize, Debug)]
pub struct Workplace {
pub name: String,
pub thumbnail: Option<String>,
pub description: String,
pub displayed: bool,
}
#[derive(Deserialize, Debug)]
pub struct Education {
pub name: String,
pub thumbnail: Option<String>,
pub description: String,
pub displayed: bool,
}
#[derive(Deserialize, Debug)]
pub struct PortfolioPageModel {
// pub title: String,
pub work_history: Vec<Workplace>,
pub education: Vec<Education>,
}
#[derive(Template)]
#[template(path = "portfolio.html")]
pub struct PortfolioTemplate {
pub title: String,
pub body: String,
pub project_list: Vec<ParseResult<ProjectMetadata>>,
pub header_props: HeaderProps,
pub workplace_list: Vec<Workplace>,
pub education_list: Vec<Education>,
pub contact_links: Vec<ContactLink>,
pub technology_list: Vec<String>,
}
pub async fn render_portfolio() -> Result<PortfolioTemplate, StatusCode> {
let portfolio = parse_post::<PortfolioPageModel>("_pages/portfolio.md").await?;
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();
let workplace_list = portfolio
.metadata
.work_history
.into_iter()
.filter(|workplace| workplace.displayed)
.collect::<Vec<Workplace>>();
let education_list = portfolio
.metadata
.education
.into_iter()
.filter(|education| education.displayed)
.collect::<Vec<Education>>();
let contact_links = vec![
ContactLink {
href: "mailto:michalvankosk@gmail.com".to_string(),
label: "michalvankosk@gmail.com".to_string(),
title: "E-mail address".to_string(),
svg: "mail".to_string(),
},
// ContactLink {
// href: "tel:+421-905-372-947".to_string(),
// label: "+421 905 372 947".to_string(),
// title: "Phone".to_string(),
// svg: "phone".to_string(),
// },
ContactLink {
href: "https://github.com/michalvankodev".to_string(),
label: "GitHub".to_string(),
title: "Github profile".to_string(),
svg: "github".to_string(),
},
ContactLink {
href: "https://www.linkedin.com/in/michal-vanko-dev/".to_string(),
label: "LinkedIn".to_string(),
title: "LinkedIn profile".to_string(),
svg: "linkedin".to_string(),
},
];
let technology_list = vec![
"Rust",
"HTMX",
"React",
"Svelte",
"Angular",
"PostgreSQL",
"Redis",
"GraphQL",
"TypeScript",
"Node.js",
"Axum",
"Bevy",
"Tailwind",
"OCaml",
"Python",
"git",
"Linux",
"Docker",
"Devops",
"Selfhosting",
]
.into_iter()
.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,
})
}

View File

@ -3,6 +3,7 @@ use axum::http::StatusCode;
use crate::{ use crate::{
components::site_header::HeaderProps, components::site_header::HeaderProps,
filters,
post_utils::{post_listing::get_post_list, post_parser::ParseResult}, post_utils::{post_listing::get_post_list, post_parser::ParseResult},
projects::project_model::ProjectMetadata, projects::project_model::ProjectMetadata,
}; };

View File

@ -13,7 +13,6 @@ pub fn generate_image_with_src(
width: u32, width: u32,
height: u32, height: u32,
suffix: &str, suffix: &str,
generate_image: bool,
) -> Result<String, anyhow::Error> { ) -> Result<String, anyhow::Error> {
let path_to_generated = get_generated_file_name(orig_img_path); let path_to_generated = get_generated_file_name(orig_img_path);
let file_stem = path_to_generated.file_stem().unwrap().to_str().unwrap(); let file_stem = path_to_generated.file_stem().unwrap().to_str().unwrap();
@ -24,32 +23,36 @@ pub fn generate_image_with_src(
let resolutions = [(width, height, 1.)]; let resolutions = [(width, height, 1.)];
let exported_formats = get_export_formats(orig_img_path); let exported_formats = get_export_formats(orig_img_path);
if exported_formats.is_empty() {
return Ok(orig_img_path.to_string());
}
let exported_format = *exported_formats.first().unwrap(); let exported_format = *exported_formats.first().unwrap();
let path_to_generated_arc = Arc::new(path_to_generated); let path_to_generated_arc = Arc::new(path_to_generated);
let path_to_generated_clone = Arc::clone(&path_to_generated_arc); let path_to_generated_clone = Arc::clone(&path_to_generated_arc);
if generate_image { rayon::spawn(move || {
rayon::spawn(move || { let orig_img = ImageReader::open(&disk_img_path)
let orig_img = ImageReader::open(&disk_img_path) .with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path))
.with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path)) .unwrap()
.unwrap() .decode()
.decode() .unwrap();
.unwrap(); let path_to_generated = path_to_generated_clone.as_ref();
let path_to_generated = path_to_generated_clone.as_ref();
let result = generate_images(
&orig_img,
path_to_generated,
&resolutions,
&[exported_format],
)
.with_context(|| "Failed to generate images".to_string());
if let Err(e) = result {
tracing::error!("Error: {}", e);
}
});
let result = generate_images(
&orig_img,
path_to_generated,
&resolutions,
&[exported_format],
)
.with_context(|| "Failed to generate images".to_string());
if let Err(e) = result {
tracing::error!("Error: {}", e);
}
});
}
let path_to_generated = Arc::clone(&path_to_generated_arc); let path_to_generated = Arc::clone(&path_to_generated_arc);
let image_path = get_image_path( let image_path = get_image_path(

View File

@ -20,7 +20,6 @@ pub fn generate_picture_markup(
height: u32, height: u32,
alt_text: &str, alt_text: &str,
class_name: Option<&str>, class_name: Option<&str>,
generate_image: bool,
) -> Result<String, anyhow::Error> { ) -> Result<String, anyhow::Error> {
let exported_formats = get_export_formats(orig_img_path); let exported_formats = get_export_formats(orig_img_path);
let class_attr = if let Some(class) = class_name { let class_attr = if let Some(class) = class_name {
@ -55,25 +54,22 @@ pub fn generate_picture_markup(
let exported_formats_arc = Arc::new(exported_formats); let exported_formats_arc = Arc::new(exported_formats);
let exported_formats_clone = Arc::clone(&exported_formats_arc); let exported_formats_clone = Arc::clone(&exported_formats_arc);
if generate_image { rayon::spawn(move || {
rayon::spawn(move || { let orig_img = ImageReader::open(&disk_img_path)
let orig_img = ImageReader::open(&disk_img_path) .with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path))
.with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path)) .unwrap()
.unwrap() .decode()
.decode() .unwrap();
.unwrap(); let path_to_generated = path_to_generated_clone.as_ref();
let path_to_generated = path_to_generated_clone.as_ref(); let resolutions = resolutions_clone.as_ref();
let resolutions = resolutions_clone.as_ref(); let exported_formats = exported_formats_clone.as_ref();
let exported_formats = exported_formats_clone.as_ref();
let result = let result = generate_images(&orig_img, path_to_generated, resolutions, exported_formats)
generate_images(&orig_img, path_to_generated, resolutions, exported_formats) .with_context(|| "Failed to generate images".to_string());
.with_context(|| "Failed to generate images".to_string()); if let Err(e) = result {
if let Err(e) = result { tracing::error!("Error: {}", e);
tracing::error!("Error: {}", e); }
} });
});
}
let exported_formats = Arc::clone(&exported_formats_arc); let exported_formats = Arc::clone(&exported_formats_arc);
let path_to_generated = Arc::clone(&path_to_generated_arc); let path_to_generated = Arc::clone(&path_to_generated_arc);
@ -314,15 +310,8 @@ fn test_generate_picture_markup() {
</picture>"#, </picture>"#,
}; };
assert_eq!( assert_eq!(
generate_picture_markup( generate_picture_markup(orig_img_path, width, height, "Testing image alt", None,)
orig_img_path, .expect("picture markup has to be generated"),
width,
height,
"Testing image alt",
None,
false
)
.expect("picture markup has to be generated"),
result result
); );
} }

View File

@ -21,7 +21,7 @@ pub async fn get_post_list<'de, Metadata: DeserializeOwned>(
let file_path = file.path(); let file_path = file.path();
let file_path_str = file_path.to_str().unwrap(); let file_path_str = file_path.to_str().unwrap();
info!(":{}", file_path_str); info!(":{}", file_path_str);
let post = parse_post::<Metadata>(file_path_str, false).await?; let post = parse_post::<Metadata>(file_path_str).await?;
posts.push(post); posts.push(post);
} }

View File

@ -1,22 +1,10 @@
use core::panic;
use std::path::Path; use std::path::Path;
use axum::http::StatusCode; use axum::http::StatusCode;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use gray_matter::{engine::YAML, Matter}; use gray_matter::{engine::YAML, Matter};
use image::image_dimensions;
use indoc::formatdoc;
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
use serde::{de::DeserializeOwned, Deserialize, Deserializer}; use serde::{de::DeserializeOwned, Deserialize, Deserializer};
use syntect::{highlighting::ThemeSet, html::highlighted_html_for_string, parsing::SyntaxSet};
use tokio::fs; use tokio::fs;
use tracing::{debug, error};
use crate::picture_generator::{
picture_markup_generator::generate_picture_markup, resolutions::get_max_resolution,
};
pub const MAX_BLOG_IMAGE_RESOLUTION: (u32, u32) = (1280, 860);
pub fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error> pub fn deserialize_date<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where where
@ -41,7 +29,6 @@ pub struct ParseResult<Metadata> {
pub async fn parse_post<'de, Metadata: DeserializeOwned>( pub async fn parse_post<'de, Metadata: DeserializeOwned>(
path: &str, path: &str,
generate_images: bool,
) -> Result<ParseResult<Metadata>, StatusCode> { ) -> Result<ParseResult<Metadata>, StatusCode> {
let file_contents = fs::read_to_string(path) let file_contents = fs::read_to_string(path)
.await .await
@ -56,8 +43,6 @@ pub async fn parse_post<'de, Metadata: DeserializeOwned>(
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
})?; })?;
let body = parse_html(&metadata.content, generate_images);
let filename = Path::new(path) let filename = Path::new(path)
.file_stem() .file_stem()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
@ -66,173 +51,8 @@ pub async fn parse_post<'de, Metadata: DeserializeOwned>(
.to_owned(); .to_owned();
Ok(ParseResult { Ok(ParseResult {
body, body: metadata.content,
metadata: metadata.data, metadata: metadata.data,
slug: filename, slug: filename,
}) })
} }
enum TextKind {
Text,
Heading(Option<String>),
Code(String),
}
pub fn parse_html(markdown: &str, generate_images: bool) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_SMART_PUNCTUATION);
options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
let mut text_kind = TextKind::Text;
let syntax_set = SyntaxSet::load_defaults_newlines();
let theme_set = ThemeSet::load_defaults();
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 {
/*
Parsing images considers `alt` attribute as inner `Text` event
Therefore the `[alt]` is rendered in html as subtitle
and the `[](url "title")` `title` is rendered as `alt` attribute
*/
Event::Start(Tag::Image {
link_type,
dest_url,
title,
id,
}) => {
if !dest_url.starts_with("/") {
return Event::Html(
formatdoc!(
r#"<img
alt="{title}"
src="{dest_url}"
/>"#
)
.into(),
);
}
let dev_only_img_path =
Path::new("static/").join(dest_url.strip_prefix("/").unwrap_or(&dest_url));
let img_dimensions = image_dimensions(&dev_only_img_path).unwrap();
let (max_width, max_height) = get_max_resolution(
img_dimensions,
MAX_BLOG_IMAGE_RESOLUTION.0,
MAX_BLOG_IMAGE_RESOLUTION.1,
);
// Place image into the content with scaled reso to a boundary
let picture_markup = generate_picture_markup(
&dest_url,
max_width,
max_height,
&title,
None,
generate_images,
)
.unwrap_or(formatdoc!(
r#"
<img
alt="{alt}"
src="{src}"
/>"#,
alt = title,
src = dest_url,
));
debug!(
"Image link_type: {:?} url: {} title: {} id: {}",
link_type, dest_url, title, id
);
Event::Html(
formatdoc!(
r#"<figure>
{picture_markup}
<figcaption>
"#,
)
.into(),
)
}
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => {
text_kind = TextKind::Code(lang.to_string());
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang)))
}
Event::Text(text) => match &text_kind {
TextKind::Code(lang) => {
// TODO Check https://github.com/trishume/syntect/pull/535 for typescript support
let lang = if ["ts".to_string(), "typescript".to_string()].contains(lang) {
"javascript"
} else {
lang
};
let syntax_reference = syntax_set
.find_syntax_by_token(lang)
.unwrap_or(syntax_set.find_syntax_plain_text());
let highlighted =
highlighted_html_for_string(&text, &syntax_set, syntax_reference, theme)
.unwrap();
Event::Html(highlighted.into())
}
TextKind::Heading(provided_id) => {
let heading_id = provided_id.clone().unwrap_or({
text.to_lowercase()
.replace(|c: char| !c.is_alphanumeric(), "-")
});
debug!("heading_id: {}", heading_id.clone());
match heading_ended {
None => {
error!("Heading should have set state");
panic!("Heading should have set state");
}
Some(true) => Event::Html(text),
Some(false) => {
heading_ended = Some(true);
Event::Html(
formatdoc!(
r##"id="{heading_id}">
{text}"##
)
.into(),
)
}
}
}
_ => Event::Text(text),
},
Event::Start(Tag::Heading {
level,
id,
classes: _,
attrs: _,
}) => {
let id_str = id.map(|id| id.to_string());
debug!("heading_start: {:?}, level: {}", &id_str, level);
text_kind = TextKind::Heading(id_str);
heading_ended = Some(false);
Event::Html(format!("<{level} ").into())
}
Event::Start(_) => event,
Event::End(TagEnd::Image) => Event::Html("</figcaption></figure>".into()),
Event::End(TagEnd::CodeBlock) => {
text_kind = TextKind::Text;
Event::End(TagEnd::CodeBlock)
}
Event::End(TagEnd::Heading(heading_level)) => {
text_kind = TextKind::Text;
heading_ended = None;
Event::End(TagEnd::Heading(heading_level))
}
_ => event,
});
// Write to String buffer
let mut html = String::new();
pulldown_cmark::html::push_html(&mut html, parser);
html
}

View File

@ -1,9 +1,10 @@
use crate::{ use crate::{
feed::render_rss_feed, feed::render_rss_feed,
pages::{ pages::{
admin::render_admin, blog_post_list::render_blog_post_list, admin::render_admin, animated_logo::render_animated_logo,
blog_post_page::render_blog_post, broadcast_list::render_broadcast_post_list, blog_post_list::render_blog_post_list, blog_post_page::render_blog_post,
contact::render_contact, index::render_index, not_found::render_not_found, broadcast_list::render_broadcast_post_list, contact::render_contact, index::render_index,
not_found::render_not_found, portfolio::render_portfolio,
project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher, project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher,
}, },
}; };
@ -22,7 +23,9 @@ pub fn get_router() -> Router {
.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/: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("/admin", get(render_admin))
.route("/feed.xml", get(render_rss_feed)) .route("/feed.xml", get(render_rss_feed))
.layer( .layer(

View File

@ -0,0 +1,20 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6915_3092)">
<path d="M143.302 165.108C149.717 154.118 152.748 142.082 152.748 130.221C131.702 142.257 104.988 143.129 82.448 130.221L47.2539 190.663C80.8663 209.894 123.881 198.469 143.302 165.108Z" fill="url(#paint0_linear_6915_3092)"/>
<path d="M56.7005 34.8919C50.2856 45.9249 47.2539 57.9174 47.2539 69.779C68.3001 57.7429 95.0142 56.8708 117.554 69.779L152.704 9.33706C119.092 -9.89447 76.0771 1.53107 56.7005 34.8919Z" fill="url(#paint1_linear_6915_3092)"/>
<path d="M152.704 130.221C131.658 142.257 104.944 143.13 82.4041 130.221C59.864 117.313 47.2539 93.8951 47.2539 69.7793C68.3001 57.7433 95.0142 56.8711 117.554 69.7793C140.094 82.6876 152.704 106.149 152.704 130.221Z" fill="#EE0000"/>
</g>
<defs>
<linearGradient id="paint0_linear_6915_3092" x1="48.3523" y1="191.011" x2="116.335" y2="69.7771" gradientUnits="userSpaceOnUse">
<stop stop-color="#F40000" stop-opacity="0.1"/>
<stop offset="1" stop-color="#F40000"/>
</linearGradient>
<linearGradient id="paint1_linear_6915_3092" x1="82.6238" y1="130.395" x2="151.917" y2="9.36607" gradientUnits="userSpaceOnUse">
<stop stop-color="#F40000"/>
<stop offset="1" stop-color="#F40000" stop-opacity="0.1"/>
</linearGradient>
<clipPath id="clip0_6915_3092">
<rect width="105.495" height="200" fill="white" transform="translate(47.2539)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

BIN
static/images/uploads/TUKE_ZNAK_B_CMYK.png (Stored with Git LFS) Normal file

Binary file not shown.

View 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

View File

@ -0,0 +1,67 @@
<svg xmlns="http://www.w3.org/2000/svg" class="u-svg-content" viewBox="0 0 184 187" x="0px" y="0px" style="width: 1em; height: 1em;"><path fill="#FEFFFE" opacity="1.000000" stroke="none" d="
M111.000000,188.000000
C74.000000,188.000000 37.500004,188.000000 1.000005,188.000000
C1.000003,125.666672 1.000003,63.333344 1.000002,1.000013
C62.333324,1.000009 123.666649,1.000009 184.999969,1.000004
C184.999985,63.333317 184.999985,125.666634 185.000000,187.999969
C160.500000,188.000000 136.000000,188.000000 111.000000,188.000000
M141.418015,133.928207
C127.767067,142.503525 114.455452,151.692993 100.367393,159.477219
C86.943886,166.894196 72.253189,170.657486 56.609383,168.880310
C55.957951,168.806305 55.270107,169.052765 53.888592,169.252869
C95.326988,189.169525 143.651657,174.202332 165.835312,135.157486
C187.973633,96.192459 174.065491,51.700832 151.058075,32.699059
C151.870972,47.383453 145.096802,58.609764 134.719620,67.665199
C127.865273,73.646515 120.554497,79.581230 112.480988,83.588264
C96.438011,91.550682 79.782196,98.298607 63.247910,105.234390
C59.770390,106.693146 57.497551,108.087372 57.806816,112.213203
C58.016811,115.014671 57.524277,117.875526 57.804279,120.663788
C58.737183,129.953735 63.131657,135.090820 72.435020,136.152100
C78.101418,136.798462 84.129677,136.459320 89.714272,135.257034
C113.780266,130.075912 135.140045,118.498703 156.131866,106.174423
C157.463760,105.392456 158.804474,104.625534 160.133087,103.856255
C160.133087,108.261963 160.524628,112.130524 159.988434,115.865921
C159.658905,118.161545 158.418015,120.841133 156.701431,122.323769
C152.050400,126.340919 146.927032,129.811172 141.418015,133.928207
M7.504056,83.100655
C5.341846,103.972412 9.647369,123.228951 22.148375,141.683990
C6.351274,81.329971 68.548477,15.291668 127.692780,16.490648
C84.716667,-7.122169 16.257736,18.354082 7.504056,83.100655
M99.857971,43.268894
C82.759735,53.964348 71.778099,69.661499 63.192806,87.355751
C80.470497,79.698120 97.136421,71.304016 110.466728,57.889004
C113.839012,54.495293 116.367683,49.897110 118.158737,45.403858
C119.866737,41.118980 117.462463,38.490543 112.833977,39.242863
C108.643143,39.924049 104.625725,41.672096 99.857971,43.268894
z"></path><path fill="#63B224" opacity="1.000000" stroke="none" d="
M141.704498,133.713257
C146.927032,129.811172 152.050400,126.340919 156.701431,122.323769
C158.418015,120.841133 159.658905,118.161545 159.988434,115.865921
C160.524628,112.130524 160.133087,108.261963 160.133087,103.856255
C158.804474,104.625534 157.463760,105.392456 156.131866,106.174423
C135.140045,118.498703 113.780266,130.075912 89.714272,135.257034
C84.129677,136.459320 78.101418,136.798462 72.435020,136.152100
C63.131657,135.090820 58.737183,129.953735 57.804279,120.663788
C57.524277,117.875526 58.016811,115.014671 57.806816,112.213203
C57.497551,108.087372 59.770390,106.693146 63.247910,105.234390
C79.782196,98.298607 96.438011,91.550682 112.480988,83.588264
C120.554497,79.581230 127.865273,73.646515 134.719620,67.665199
C145.096802,58.609764 151.870972,47.383453 151.058075,32.699059
C174.065491,51.700832 187.973633,96.192459 165.835312,135.157486
C143.651657,174.202332 95.326988,189.169525 53.888592,169.252869
C55.270107,169.052765 55.957951,168.806305 56.609383,168.880310
C72.253189,170.657486 86.943886,166.894196 100.367393,159.477219
C114.455452,151.692993 127.767067,142.503525 141.704498,133.713257
z"></path><path fill="#64B225" opacity="1.000000" stroke="none" d="
M7.593564,82.658463
C16.257736,18.354082 84.716667,-7.122169 127.692780,16.490648
C68.548477,15.291668 6.351274,81.329971 22.148375,141.683990
C9.647369,123.228951 5.341846,103.972412 7.593564,82.658463
z"></path><path fill="#64B226" opacity="1.000000" stroke="none" d="
M100.194901,43.109314
C104.625725,41.672096 108.643143,39.924049 112.833977,39.242863
C117.462463,38.490543 119.866737,41.118980 118.158737,45.403858
C116.367683,49.897110 113.839012,54.495293 110.466728,57.889004
C97.136421,71.304016 80.470497,79.698120 63.192806,87.355751
C71.778099,69.661499 82.759735,53.964348 100.194901,43.109314
z"></path></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View 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

BIN
static/images/uploads/logo_spse_far.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,4 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M477 256C477 312.348 476.994 354.269 473.509 385.636C470.02 417.033 463.118 436.975 450.047 450.047C436.975 463.118 417.033 470.02 385.636 473.509C354.269 476.994 312.348 477 256 477C199.652 477 157.731 476.994 126.364 473.509C94.9671 470.02 75.0246 463.118 61.9534 450.047C48.8822 436.975 41.9797 417.033 38.4912 385.636C35.0059 354.269 35 312.348 35 256C35 199.652 35.0059 157.731 38.4912 126.364C41.9797 94.9671 48.8822 75.0246 61.9534 61.9534C75.0246 48.8822 94.9671 41.9797 126.364 38.4912C157.731 35.0059 199.652 35 256 35C312.348 35 354.269 35.0059 385.636 38.4912C417.033 41.9797 436.975 48.8822 450.047 61.9534C463.118 75.0246 470.02 94.9671 473.509 126.364C476.994 157.731 477 199.652 477 256Z" fill="#D8F6FF" stroke="#32A8EB" stroke-width="8" stroke-linejoin="round"/>
<path d="M347.694 326.566H360V235.186C360 203.566 340.791 185 310.176 185C290.066 185 270.257 193.413 256.15 208.208C248.046 193.123 232.739 185 212.629 185C194.62 185 177.212 191.382 163.405 202.696L161.004 187.031H152V326.566H164.306V212.269C176.912 201.535 193.12 195.153 209.628 195.153C234.84 195.153 249.847 210.528 249.847 236.927V326.566H262.153V235.186C262.153 227.934 261.253 221.262 259.152 215.46C272.058 202.696 289.466 195.153 307.175 195.153C332.687 195.153 347.694 210.528 347.694 236.927V326.566Z" fill="#32A8EB"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.cls-1{fill:#00195f;}</style></defs><path class="cls-1" d="M26.83,27.56c10-8.09,24.42-10.84,32.42-11A31.33,31.33,0,0,0,1.77,40.23L17,22.29h1.12l1.69,12a53.81,53.81,0,0,1,7-6.76ZM58.56,40.09A2,2,0,0,1,58,41.56a2,2,0,0,1-1.48.62h-1a1.84,1.84,0,0,1-2.09-2.09h1.05a1.21,1.21,0,0,0,0,.18.74.74,0,0,0,.33.65,1.26,1.26,0,0,0,.73.22,1.07,1.07,0,0,0,.81-.27,1.18,1.18,0,0,0,.24-.79,1,1,0,0,0-.31-.74L54,36.85a2.18,2.18,0,0,1-.61-1.48v-.52A2,2,0,0,1,54,33.37a2,2,0,0,1,1.48-.62h1c1.31,0,2,.7,2.09,2.1h-1c-.09-.71-.42-1.06-1-1.06a1.18,1.18,0,0,0-.84.27,1,1,0,0,0-.27.79,1.12,1.12,0,0,0,.3.74L58,38.08a2.24,2.24,0,0,1,.61,1.48v.53h0Zm-9.27,0a2.1,2.1,0,0,1-2.09,2.09H46.15a1.84,1.84,0,0,1-2.09-2.09h1a1.21,1.21,0,0,0,0,.18.74.74,0,0,0,.33.65,1.26,1.26,0,0,0,.73.22,1,1,0,0,0,.8-.27,1.18,1.18,0,0,0,.24-.79,1,1,0,0,0-.31-.74l-2.22-2.49a2.18,2.18,0,0,1-.61-1.48v-.52a2.1,2.1,0,0,1,2.09-2.1H47.2c1.31,0,2,.7,2.09,2.1h-1c-.09-.71-.42-1.06-1-1.06a1.18,1.18,0,0,0-.84.27,1,1,0,0,0-.27.79,1.08,1.08,0,0,0,.31.74l2.22,2.49a2.2,2.2,0,0,1,.61,1.48ZM40.08,42H34.94V32.78h5.14v1H37V36.9h2.06v1H37V41h3.08v1ZM63.32,32a31.23,31.23,0,0,0-4-15.26c-7.5,1.71-15.18,4.46-21.14,8.62-6.72,4.69-11.39,10.56-12.86,15.53l-.11.33L25,42H14.59L13,29.62,2.35,42.11A31.32,31.32,0,0,0,63.32,32Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/images/uploads/tuke_znak_b_cmyk.png (Stored with Git LFS) Normal file

Binary file not shown.

8
static/resources/anime.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -64,11 +64,6 @@ collections:
fields: fields:
- { label: Title, name: title, widget: string } - { label: Title, name: title, widget: string }
- { label: Body, name: body, widget: markdown } - { label: Body, name: body, widget: markdown }
- {
label: Work history prelude,
name: work_history_prelude,
widget: markdown,
}
- label: Work history - label: Work history
name: work_history name: work_history
widget: list widget: list
@ -85,37 +80,7 @@ collections:
- { label: City, name: city, widget: string, required: false } - { label: City, name: city, widget: string, required: false }
- { label: Country, name: country, widget: string, required: false } - { label: Country, name: country, widget: string, required: false }
- { label: Displayed, name: displayed, widget: boolean, default: true } - { label: Displayed, name: displayed, widget: boolean, default: true }
- label: Projects - { label: Thumbnail, name: thumbnail, widget: image, required: false }
name: projects
widget: list
fields:
- { label: Project name, name: name, widget: string }
- {
label: Displayed,
name: displayed,
widget: boolean,
default: true,
}
- { label: Description, name: body, widget: markdown }
- {
label: Cover image,
name: cover_image,
widget: image,
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 - label: Education
name: education name: education
widget: list widget: list
@ -128,6 +93,7 @@ collections:
default: true, default: true,
} }
- { label: Description, name: description, widget: markdown } - { label: Description, name: description, widget: markdown }
- { label: Thumbnail, name: thumbnail, widget: image, required: false }
- name: 'projects' # Used in routes, e.g., /admin/collections/blog - name: 'projects' # Used in routes, e.g., /admin/collections/blog
label: 'Showcase projects' # Used in the UI label: 'Showcase projects' # Used in the UI
folder: '_projects/' # The path to the folder where the documents are stored folder: '_projects/' # The path to the folder where the documents are stored

View 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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" 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"/></svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@ -266,3 +266,18 @@ article a {
transform: translate(0, 0); transform: translate(0, 0);
} }
} }
@view-transition {
navigation: auto;
}
/* Define the animation for persistent elements (like the header and title) */
/* ::view-transition-group(*) { */
/* transition: transform 3.4s ease-in-out; */
/* } */
/* ::view-transition-group(blog_post_preview) { */
/* animation-duration: 5.5s; */
/* transition: transform 5.4s ease-in-out; */
/* opacity: 1; */
/* } */

View File

@ -107,7 +107,7 @@
} }
/* /*
! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com ! tailwindcss v3.4.15 | MIT License | https://tailwindcss.com
*/ */
/* /*
@ -550,7 +550,7 @@ video {
/* Make elements with the HTML hidden attribute stay hidden by default */ /* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] { [hidden]:where(:not([hidden="until-found"])) {
display: none; display: none;
} }
@ -620,6 +620,10 @@ video {
size-adjust: 92%; size-adjust: 92%;
} }
.visible {
visibility: visible;
}
.col-span-2 { .col-span-2 {
grid-column: span 2 / span 2; grid-column: span 2 / span 2;
} }
@ -640,6 +644,10 @@ video {
margin: 0.25rem; margin: 0.25rem;
} }
.m-10 {
margin: 2.5rem;
}
.m-4 { .m-4 {
margin: 1rem; margin: 1rem;
} }
@ -662,6 +670,11 @@ video {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.mx-4 { .mx-4 {
margin-left: 1rem; margin-left: 1rem;
margin-right: 1rem; margin-right: 1rem;
@ -682,6 +695,11 @@ video {
margin-right: auto; margin-right: auto;
} }
.my-1 {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
.my-2 { .my-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -702,11 +720,6 @@ video {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.mb-1 { .mb-1 {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
@ -739,6 +752,14 @@ video {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
.mt-4 {
margin-top: 1rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.block { .block {
display: block; display: block;
} }
@ -779,10 +800,18 @@ video {
height: 240px; height: 240px;
} }
.h-\[320px\] {
height: 320px;
}
.h-auto { .h-auto {
height: auto; height: auto;
} }
.h-full {
height: 100%;
}
.max-h-\[236px\] { .max-h-\[236px\] {
max-height: 236px; max-height: 236px;
} }
@ -807,6 +836,18 @@ video {
width: 180px; width: 180px;
} }
.w-\[320px\] {
width: 320px;
}
.w-0 {
width: 0px;
}
.max-w-\[24rem\] {
max-width: 24rem;
}
.max-w-\[32rem\] { .max-w-\[32rem\] {
max-width: 32rem; max-width: 32rem;
} }
@ -827,6 +868,10 @@ video {
flex: 1 1 0%; flex: 1 1 0%;
} }
.shrink-0 {
flex-shrink: 0;
}
.flex-grow { .flex-grow {
flex-grow: 1; flex-grow: 1;
} }
@ -853,6 +898,10 @@ video {
flex-direction: column; flex-direction: column;
} }
.flex-wrap {
flex-wrap: wrap;
}
.place-content-center { .place-content-center {
place-content: center; place-content: center;
} }
@ -885,6 +934,10 @@ video {
gap: 0.5rem; gap: 0.5rem;
} }
.gap-4 {
gap: 1rem;
}
.gap-6 { .gap-6 {
gap: 1.5rem; gap: 1.5rem;
} }
@ -921,39 +974,53 @@ video {
border-width: 2px; border-width: 2px;
} }
.border-l {
border-left-width: 1px;
}
.border-blue-300 {
--tw-border-opacity: 1;
border-color: rgb(130 195 247 / var(--tw-border-opacity, 1));
}
.border-blue-500 { .border-blue-500 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(23 137 224 / var(--tw-border-opacity)); border-color: rgb(23 137 224 / var(--tw-border-opacity, 1));
} }
.border-blue-950 { .border-blue-950 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(11 39 70 / var(--tw-border-opacity)); border-color: rgb(11 39 70 / var(--tw-border-opacity, 1));
} }
.border-slate-300 { .border-slate-300 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(203 213 225 / var(--tw-border-opacity)); border-color: rgb(203 213 225 / var(--tw-border-opacity, 1));
}
.border-l-slate-300 {
--tw-border-opacity: 1;
border-left-color: rgb(203 213 225 / var(--tw-border-opacity, 1));
} }
.bg-blue-100 { .bg-blue-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(225 239 253 / var(--tw-bg-opacity)); background-color: rgb(225 239 253 / var(--tw-bg-opacity, 1));
} }
.bg-blue-50 { .bg-blue-50 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(241 247 254 / var(--tw-bg-opacity)); background-color: rgb(241 247 254 / var(--tw-bg-opacity, 1));
} }
.bg-pink-200 { .bg-pink-200 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 207 247 / var(--tw-bg-opacity)); background-color: rgb(255 207 247 / var(--tw-bg-opacity, 1));
} }
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
} }
.fill-blue-900 { .fill-blue-900 {
@ -968,6 +1035,10 @@ video {
padding: 0.125rem; padding: 0.125rem;
} }
.p-2 {
padding: 0.5rem;
}
.p-3 { .p-3 {
padding: 0.75rem; padding: 0.75rem;
} }
@ -1007,6 +1078,10 @@ video {
text-align: justify; text-align: justify;
} }
.font-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.text-2xl { .text-2xl {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
@ -1027,6 +1102,11 @@ video {
line-height: 1; line-height: 1;
} }
.text-9xl {
font-size: 8rem;
line-height: 1;
}
.text-base { .text-base {
font-size: 1rem; font-size: 1rem;
line-height: 1.5rem; line-height: 1.5rem;
@ -1047,16 +1127,6 @@ video {
line-height: 1.75rem; line-height: 1.75rem;
} }
.text-6xl {
font-size: 3.75rem;
line-height: 1;
}
.text-9xl {
font-size: 8rem;
line-height: 1;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
@ -1085,39 +1155,48 @@ video {
line-height: 1.25rem; line-height: 1.25rem;
} }
.leading-tight {
line-height: 1.25;
}
.text-blue-500 { .text-blue-500 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(23 137 224 / var(--tw-text-opacity)); color: rgb(23 137 224 / var(--tw-text-opacity, 1));
} }
.text-blue-700 { .text-blue-700 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(10 86 154 / var(--tw-text-opacity)); color: rgb(10 86 154 / var(--tw-text-opacity, 1));
} }
.text-blue-900 { .text-blue-900 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(16 62 106 / var(--tw-text-opacity)); color: rgb(16 62 106 / var(--tw-text-opacity, 1));
} }
.text-blue-950 { .text-blue-950 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(11 39 70 / var(--tw-text-opacity)); color: rgb(11 39 70 / var(--tw-text-opacity, 1));
}
.text-pink-900 {
--tw-text-opacity: 1;
color: rgb(119 24 89 / var(--tw-text-opacity, 1));
} }
.text-pink-950 { .text-pink-950 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(80 2 56 / var(--tw-text-opacity)); color: rgb(80 2 56 / var(--tw-text-opacity, 1));
} }
.text-slate-600 { .text-slate-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity)); color: rgb(71 85 105 / var(--tw-text-opacity, 1));
} }
.text-slate-800 { .text-slate-800 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(30 41 59 / var(--tw-text-opacity)); color: rgb(30 41 59 / var(--tw-text-opacity, 1));
} }
.no-underline { .no-underline {
@ -1132,14 +1211,14 @@ video {
a { a {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(146 22 110 / var(--tw-text-opacity)); color: rgb(146 22 110 / var(--tw-text-opacity, 1));
text-decoration-line: underline; text-decoration-line: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
} }
a:hover { a:hover {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(23 137 224 / var(--tw-text-opacity)); color: rgb(23 137 224 / var(--tw-text-opacity, 1));
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
@ -1178,7 +1257,7 @@ strong {
} }
h1 { h1 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(16 62 106 / var(--tw-text-opacity)); color: rgb(16 62 106 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
h1 { h1 {
@ -1218,7 +1297,7 @@ strong {
} }
h2 { h2 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(16 62 106 / var(--tw-text-opacity)); color: rgb(16 62 106 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
h2 { h2 {
@ -1278,7 +1357,7 @@ strong {
} }
h3 { h3 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(16 62 106 / var(--tw-text-opacity)); color: rgb(16 62 106 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
h3 { h3 {
@ -1338,7 +1417,7 @@ strong {
} }
h4 { h4 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(16 62 106 / var(--tw-text-opacity)); color: rgb(16 62 106 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
h4 { h4 {
@ -1392,7 +1471,7 @@ strong {
} }
p { p {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(2 6 23 / var(--tw-text-opacity)); color: rgb(2 6 23 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
p { p {
@ -1472,7 +1551,7 @@ strong {
} }
figcaption { figcaption {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(12 73 128 / var(--tw-text-opacity)); color: rgb(12 73 128 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
figcaption { figcaption {
@ -1516,7 +1595,7 @@ strong {
} }
table { table {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(226 232 240 / var(--tw-border-opacity)); border-color: rgb(226 232 240 / var(--tw-border-opacity, 1));
} }
table { table {
font-size: 0.875rem; font-size: 0.875rem;
@ -1542,11 +1621,11 @@ strong {
} }
thead { thead {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(225 239 253 / var(--tw-bg-opacity)); background-color: rgb(225 239 253 / var(--tw-bg-opacity, 1));
} }
tbody { tbody {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(248 250 252 / var(--tw-bg-opacity)); background-color: rgb(248 250 252 / var(--tw-bg-opacity, 1));
} }
td, td,
th { th {
@ -1578,7 +1657,7 @@ strong {
} }
tr:nth-child(even) { tr:nth-child(even) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(241 245 249 / var(--tw-bg-opacity)); background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));
} }
blockquote { blockquote {
margin-left: 1.5rem; margin-left: 1.5rem;
@ -1592,11 +1671,11 @@ strong {
} }
blockquote { blockquote {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(215 34 169 / var(--tw-border-opacity)); border-color: rgb(215 34 169 / var(--tw-border-opacity, 1));
} }
blockquote { blockquote {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 244 253 / var(--tw-bg-opacity)); background-color: rgb(255 244 253 / var(--tw-bg-opacity, 1));
} }
blockquote { blockquote {
padding-top: 0.25rem; padding-top: 0.25rem;
@ -1622,7 +1701,7 @@ strong {
} }
p { p {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(71 85 105 / var(--tw-text-opacity)); color: rgb(71 85 105 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
p { p {
@ -1639,11 +1718,11 @@ strong {
} }
:not(pre) code { :not(pre) code {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(130 195 247 / var(--tw-border-opacity)); border-color: rgb(130 195 247 / var(--tw-border-opacity, 1));
} }
:not(pre) code { :not(pre) code {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(225 239 253 / var(--tw-bg-opacity)); background-color: rgb(225 239 253 / var(--tw-bg-opacity, 1));
} }
:not(pre) code { :not(pre) code {
padding-left: 0.25rem; padding-left: 0.25rem;
@ -1659,7 +1738,7 @@ strong {
} }
:not(pre) code { :not(pre) code {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(119 24 89 / var(--tw-text-opacity)); color: rgb(119 24 89 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
:not(pre) code { :not(pre) code {
@ -1727,7 +1806,7 @@ strong {
ul, ul,
ol { ol {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(2 6 23 / var(--tw-text-opacity)); color: rgb(2 6 23 / var(--tw-text-opacity, 1));
} }
@media (min-width: 768px) { @media (min-width: 768px) {
ul, ul,
@ -1913,6 +1992,32 @@ article a:visited {
} }
} }
@view-transition {
navigation: auto;
}
/* Define the animation for persistent elements (like the header and title) */
/* ::view-transition-group(*) { */
/* transition: transform 3.4s ease-in-out; */
/* } */
/* ::view-transition-group(blog_post_preview) { */
/* animation-duration: 5.5s; */
/* transition: transform 5.4s ease-in-out; */
/* opacity: 1; */
/* } */
.first\:border-l-0:first-child {
border-left-width: 0px;
}
.visited\:text-blue-950:visited { .visited\:text-blue-950:visited {
color: rgb(11 39 70 ); color: rgb(11 39 70 );
} }
@ -1923,7 +2028,7 @@ article a:visited {
.hover\:bg-pink-200:hover { .hover\:bg-pink-200:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 207 247 / var(--tw-bg-opacity)); background-color: rgb(255 207 247 / var(--tw-bg-opacity, 1));
} }
.hover\:fill-blue-400:hover { .hover\:fill-blue-400:hover {
@ -1983,11 +2088,24 @@ article a:visited {
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.md\:mx-6 {
margin-left: 1.5rem;
margin-right: 1.5rem;
}
.md\:my-8 { .md\:my-8 {
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.md\:block {
display: block;
}
.md\:grid {
display: grid;
}
.md\:h-16 { .md\:h-16 {
height: 4rem; height: 4rem;
} }
@ -2000,6 +2118,10 @@ article a:visited {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.md\:grid-cols-\[1fr_auto\] {
grid-template-columns: 1fr auto;
}
.md\:grid-rows-\[masonry\] { .md\:grid-rows-\[masonry\] {
grid-template-rows: masonry; grid-template-rows: masonry;
} }
@ -2032,6 +2154,11 @@ article a:visited {
line-height: 2.5rem; line-height: 2.5rem;
} }
.md\:text-5xl {
font-size: 3rem;
line-height: 1;
}
.md\:text-6xl { .md\:text-6xl {
font-size: 3.75rem; font-size: 3.75rem;
line-height: 1; line-height: 1;
@ -2051,6 +2178,10 @@ article a:visited {
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1.75rem; line-height: 1.75rem;
} }
.md\:leading-tight {
line-height: 1.25;
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@ -2080,6 +2211,14 @@ article a:visited {
margin-top: 5rem; margin-top: 5rem;
} }
.lg\:mt-8 {
margin-top: 2rem;
}
.lg\:mb-10 {
margin-bottom: 2.5rem;
}
.lg\:block { .lg\:block {
display: block; display: block;
} }
@ -2122,6 +2261,11 @@ article a:visited {
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.75rem; line-height: 1.75rem;
} }
.lg\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
} }
@media (min-width: 1280px) { @media (min-width: 1280px) {
@ -2129,6 +2273,10 @@ article a:visited {
grid-column: auto; grid-column: auto;
} }
.xl\:col-span-2 {
grid-column: span 2 / span 2;
}
.xl\:row-span-2 { .xl\:row-span-2 {
grid-row: span 2 / span 2; grid-row: span 2 / span 2;
} }
@ -2137,10 +2285,42 @@ article a:visited {
grid-row-start: auto; grid-row-start: auto;
} }
.xl\:mb-4 {
margin-bottom: 1rem;
}
.xl\:mt-10 {
margin-top: 2.5rem;
}
.xl\:block { .xl\:block {
display: block; display: block;
} }
.xl\:flex {
display: flex;
}
.xl\:grid {
display: grid;
}
.xl\:hidden {
display: none;
}
.xl\:auto-cols-auto {
grid-auto-columns: auto;
}
.xl\:auto-cols-fr {
grid-auto-columns: minmax(0, 1fr);
}
.xl\:grid-flow-col {
grid-auto-flow: column;
}
.xl\:grid-cols-3 { .xl\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
@ -2149,13 +2329,60 @@ article a:visited {
grid-template-columns: 1fr 2fr; grid-template-columns: 1fr 2fr;
} }
.xl\:justify-start {
justify-content: flex-start;
}
.xl\:gap-8 {
gap: 2rem;
}
.xl\:gap-5 {
gap: 1.25rem;
}
.xl\:gap-10 {
gap: 2.5rem;
}
.xl\:gap-x-32 { .xl\:gap-x-32 {
-moz-column-gap: 8rem; -moz-column-gap: 8rem;
column-gap: 8rem; column-gap: 8rem;
} }
.xl\:border-l {
border-left-width: 1px;
}
.xl\:border-slate-300 {
--tw-border-opacity: 1;
border-color: rgb(203 213 225 / var(--tw-border-opacity, 1));
}
.xl\:text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.xl\:text-7xl {
font-size: 4.5rem;
line-height: 1;
}
.xl\:font-medium {
font-weight: 500;
}
.xl\:first\:border-l-0:first-child {
border-left-width: 0px;
}
} }
@media print { @media print {
.print\:inline {
display: inline;
}
.print\:hidden { .print\:hidden {
display: none; display: none;
} }

View File

@ -0,0 +1,81 @@
{% include "icons/m-logo-animated.svg" %}
<script src="/resources/anime.min.js"></script>
<script>
var svg = document.getElementById("m-logo")
svg.setAttribute("visibility", "visible");
var borderTimeline = anime.timeline({
duration: 2000,
easing: 'easeInOutSine',
})
borderTimeline.add({
targets: '#m-logo #border-start',
strokeDashoffset: [anime.setDashoffset, -310],
duration: 2000,
easing: 'easeOutExpo',
begin: (animation) => {
const target = animation.animatables[0].target
target.setAttribute("visibility", "visible")
}
}, 0)
borderTimeline.add({
targets: '#m-logo #m-letter-start',
strokeDashoffset: [anime.setDashoffset, -447.4],
easing: 'easeOutExpo',
duration: 2000,
begin: (animation) => {
const target = animation.animatables[0].target
target.setAttribute("visibility", "visible")
}
}, 0)
borderTimeline.add({
targets: '#m-logo #border',
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 2000,
complete: (animation) => {
//debugger;
const target = animation.animatables[0].target
target.setAttribute("fill", "url('#bg-gradient')")
var bgAnimationTimeline = anime.timeline()
bgAnimationTimeline.add({
targets: '#m-logo #bg-gradient #bg-stop',
offset: "0%",
stopColor: "rgba(216, 246, 255, 0.8)",
easing: 'easeInQuint',
duration: 123,
})
bgAnimationTimeline.add({
targets: '#m-logo #bg-gradient #bg-stop',
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)
borderTimeline.add({
targets: '#m-logo #m-letter',
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1800,
}, 160)
borderTimeline.add({
targets: '#m-logo #m-letter',
easing: 'easeInOutSine',
duration: 333,
fill: "#32a8eb",
strokeWidth: "0",
})
</script>

View File

@ -16,6 +16,10 @@
href="/feed.xml" href="/feed.xml"
/> />
<!-- Mastodon -->
<link rel="me" href="https://mastodon.online/@michalvankodev" />
<meta name="fediverse:creator" content="@michalvankodev@mastodon.online">
{% block og_meta %} {% block og_meta %}
<meta property="og:title" content="{% block title %} {{ title }} {% endblock %} @michalvankodev" /> <meta property="og:title" content="{% block title %} {{ title }} {% endblock %} @michalvankodev" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />

View File

@ -6,7 +6,7 @@
<meta property="og:url" content="https://michalvanko.dev/{{segment}}/{{slug}}" /> <meta property="og:url" content="https://michalvanko.dev/{{segment}}/{{slug}}" />
{% match thumbnail %} {% match thumbnail %}
{% when Some with (img) %} {% when Some with (img) %}
{% let src = crate::picture_generator::image_src_generator::generate_image_with_src(img, 1200, 630, "_og", true).unwrap_or("thumbnail not found".to_string())|safe %} {% let src = crate::picture_generator::image_src_generator::generate_image_with_src(img, 1200, 630, "_og").unwrap_or("thumbnail not found".to_string())|safe %}
<meta property="og:image" content="https://michalvanko.dev{{src}}" /> <meta property="og:image" content="https://michalvanko.dev{{src}}" />
{% when None %} {% when None %}
<meta property="og:image" content="https://michalvanko.dev/images/m-logo.svg" /> <meta property="og:image" content="https://michalvanko.dev/images/m-logo.svg" />
@ -17,23 +17,48 @@
{% block content %} {% block content %}
<article class="mb-6"> <article class="mb-6">
<header class="px-4 max-w-read mx-auto"> <header class="px-4 max-w-read mx-auto">
<h1 class="text-3xl md:text-4xl lg:text-6xl lg:mt-20 text-blue-900 mb-3 font-bold">{{title}}</h1> <h1 class="text-3xl md:text-4xl lg:text-6xl lg:mt-20 text-blue-900 mb-3 font-bold" style="view-transition-name: post_title_{{slug}};">{{title}}</h1>
<aside class="flex justify-between flex-row"> <aside class="flex justify-between flex-row">
{% include "post_tag_list.html" %} {% include "post_tag_list.html" %}
<section class="created-at m-1 text-right text-sm text-slate-600 md:text-lg"> <section class="created-at m-1 text-right text-sm text-slate-600 md:text-lg">
<span>Published on</span> <span>Published on</span>
<time datetime="{date}"> {{date|pretty_date}} </time> <time datetime="{date}" style="view-transition-name: post_date_{{slug}}"> {{date|pretty_date}} </time>
</section> </section>
</aside> </aside>
</header> </header>
<section class="article-body"> <section class="article-body">
{{body|escape("none")}} {{body|parse_markdown|safe}}
</section> </section>
</article> </article>
<!-- TODO: Next recommendations for reading --> <!-- 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 text-2xl md:text-2xl lg:text-4xl lg:mt-8 text-blue-900 lg:mb-10 font-bold">Further reading</h2>
<ul class="mx-5 xl:flex xl:justify-start xl:gap-10">
{% 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 %} {% endblock %}
<!-- xl:border-l xl:border-slate-300 xl:first:border-l-0 xl: -->

View File

@ -1,8 +1,11 @@
<article class="sm:grid sm:grid-cols-[max-content_1fr] sm:grid-rows-[max-content_1fr_max-content] sm:grid-flow-col sm:gap-4 md:gap-x-8 break-inside-avoid clear-both sm:clear-none"> <article
class="sm:grid sm:grid-cols-[max-content_1fr] sm:grid-rows-[max-content_1fr_max-content] sm:grid-flow-col sm:gap-4 md:gap-x-8 break-inside-avoid clear-both sm:clear-none"
style="view-transition-name:blog_post_preview_{{post.slug}};"
>
<aside class="row-span-3 self-center float-start sm:float-none mr-3 mb-3 sm:ml-0 sm:mb-0"> <aside class="row-span-3 self-center float-start sm:float-none mr-3 mb-3 sm:ml-0 sm:mb-0">
{% match post.metadata.thumbnail %} {% match post.metadata.thumbnail %}
{% when Some with (orig_path) %} {% when Some with (orig_path) %}
{{ crate::picture_generator::picture_markup_generator::generate_picture_markup(orig_path, 180, 240, "Article thumbnail", None, true).unwrap_or("thumbnail not found".to_string())|safe }} {{ crate::picture_generator::picture_markup_generator::generate_picture_markup(orig_path, 180, 240, "Article thumbnail", None).unwrap_or("thumbnail not found".to_string())|safe }}
{% when None %} {% when None %}
<div> <div>
{% include "components/blog_post_default_thumbnail.html" %} {% include "components/blog_post_default_thumbnail.html" %}
@ -10,13 +13,13 @@
{% endmatch %} {% endmatch %}
</aside> </aside>
<header> <header>
<h3 class="text-lg font-bold mb-1 md:text-3xl"> <h3 class="text-lg font-bold mb-1 md:text-3xl" style="view-transition-name: post_title_{{post.slug}}">
<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|description_filter|safe}}</section> <section class="text-base leading-5 text-slate-800 md:text-xl text-justify">{{post.body|truncate_md(2)|parse_markdown|safe}}</section>
<footer class="text-sm md:text-base lg:text-lg mt-3 sm:mt-0 clear-both sm:clear-none"> <footer class="text-sm md:text-base lg:text-lg mt-3 sm:mt-0 clear-both sm:clear-none">
<ul class="inline-block"> <ul class="inline-block" style="view-transition-name: post_tags_{{post.slug}}">
{% for tag in post.metadata.tags %} {% for tag in post.metadata.tags %}
<li class="inline-block"> <li class="inline-block">
<a href="/{{segment}}/tags/{{tag}}" class="text-pink-950 no-underline">#{{tag|capitalize}}</a> <a href="/{{segment}}/tags/{{tag}}" class="text-pink-950 no-underline">#{{tag|capitalize}}</a>
@ -24,7 +27,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
| |
<time datetime="{{post.metadata.date}}" class="text-pink-950"> {{post.metadata.date|pretty_date}} </time> <time datetime="{{post.metadata.date}}" class="text-pink-950" style="view-transition-name: post_date_{{post.slug}}" > {{post.metadata.date|pretty_date}} </time>
</footer> </footer>
</article> </article>

View File

@ -0,0 +1,18 @@
<section class="flex border rounded bg-white p-3">
<aside class="flex justify-center items-center pr-3 shrink-0">
{% match education.thumbnail %}
{% when Some with (source) %}
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "education cover", Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
<figure class="mx-4 my-2">
{{picture|safe}}
</figure>
{% when None %}
{% endmatch %}
</aside>
<section>
<header>
<h3 class="text-lg font-medium mb-1 md:text-2xl">{{education.name}}</h3>
</header>
<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{education.description|parse_markdown|safe}}</section>
</section>
</section>

View File

@ -1,4 +1,4 @@
<section class="border rounded-md bg-white p-4 break-inside-avoid"> <section class="border rounded-md bg-white p-4 break-inside-avoid" style="view-transition-name: project_preview_{{project.slug}};">
<header class="px-4 mb-3"> <header class="px-4 mb-3">
<h2 class="text-xl font-semibold text-blue-900 md:text-2xl"> <h2 class="text-xl font-semibold text-blue-900 md:text-2xl">
{% match project.metadata.link %} {% match project.metadata.link %}
@ -10,15 +10,15 @@
{{project.metadata.title}} {{project.metadata.title}}
{% endmatch %} {% endmatch %}
</h2> </h2>
<section class="description text-slate-800 my-2 md:text-xl text-justify"> <section class="description text-slate-800 my-2 md:text-xl text-justify leading-tight md:leading-tight">
{{project.body|safe}} {{project.body|parse_markdown|safe}}
</section> </section>
</header> </header>
<!-- <hr class="border-blue-950 my-5"> --> <!-- <hr class="border-blue-950 my-5"> -->
{% match project.metadata.cover_image %} {% match project.metadata.cover_image %}
{% when Some with (source) %} {% when Some with (source) %}
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 420, 236, "Project cover", Some("max-h-[236px]"), true).unwrap_or("cover not found".to_string()) %} {% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 420, 236, "Project cover", Some("max-h-[236px]")).unwrap_or("cover not found".to_string()) %}
<figure class="mx-4 my-2 flex justify-center"> <figure class="mx-4 my-2 flex justify-center">
{% match project.metadata.link %} {% match project.metadata.link %}
{% when Some with (href) %} {% when Some with (href) %}
@ -28,7 +28,6 @@
{% when None %} {% when None %}
{{picture|safe}} {{picture|safe}}
{% endmatch %} {% endmatch %}
<!-- TODO <figure> generate_image -->
</figure> </figure>
{% when None %} {% when None %}
{% endmatch %} {% endmatch %}

View File

@ -0,0 +1,19 @@
<section class="flex border 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()) %}
<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">{{skill.name}}</h3>
</header>
<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{skill.description|parse_markdown|safe}}</section>
</section>
</section>

View File

@ -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"), true).unwrap_or("thumbnail not found".to_string())|safe }} {{ crate::picture_generator::picture_markup_generator::generate_picture_markup(img, 360, 128, alt_text, Some("h-auto mx-auto rounded-sm")).unwrap_or("thumbnail not found".to_string())|safe }}
</a> </a>
{% endmacro %} {% endmacro %}

View File

@ -1,6 +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 rounded bg-white m-4 p-3 max-w-[32rem]">
<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}}" />
@ -10,9 +9,8 @@
<header> <header>
<h3 class="text-lg font-medium mb-1 md:text-2xl">{{heading}}</h3> <h3 class="text-lg font-medium mb-1 md:text-2xl">{{heading}}</h3>
</header> </header>
<p class="text-sm leading-5 text-slate-800 md:text-lg">{{description|safe}}</p> <p class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{description|safe}}</p>
</section> </section>
</section> </section>
{% endmacro %} {% endmacro %}

View File

@ -0,0 +1,18 @@
<section class="flex border rounded bg-white p-3">
<aside class="flex justify-center items-center pr-3 shrink-0">
{% match workplace.thumbnail %}
{% when Some with (source) %}
{% let picture = crate::picture_generator::picture_markup_generator::generate_picture_markup(source, 64, 64, "Workplace cover", Some("h-12 w-12 md:h-16 md:w-16")).unwrap_or("cover not found".to_string()) %}
<figure class="mx-4 my-2">
{{picture|safe}}
</figure>
{% when None %}
{% endmatch %}
</aside>
<section>
<header>
<h3 class="text-lg font-medium mb-1 md:text-2xl">{{workplace.name}}</h3>
</header>
<section class="text-sm leading-tight text-slate-800 md:text-lg md:leading-tight">{{workplace.description|parse_markdown|safe}}</section>
</section>
</section>

View File

@ -15,7 +15,7 @@
title="{{link.title}}" title="{{link.title}}"
> >
<svg aria-hidden="true" class="h-6 w-6 mx-2 self-start"> <svg aria-hidden="true" class="h-6 w-6 mx-2 self-start">
<use xlink:href="/svg/icons-sprite.svg#{{link.svg}}" /> <use xlink:href="#{{link.svg}}" />
</svg> </svg>
<span class="text-lg font-semibold">{{link.label}}</span> <span class="text-lg font-semibold">{{link.label}}</span>
</a> </a>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="m-logo"
width="512"
height="512"
viewBox="0 0 512 512"
fill="none"
version="1.1"
sodipodi:docname="m-logo-animated.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
visibility="hidden"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<defs id="custom-defs">
<radialGradient id="bg-gradient" r="91%">
<stop id="bg-stop" offset="100%" stop-color="rgba(216, 246, 255, 0)" />
<stop offset="100%" stop-color="#32a8eb" />
</radialGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.6089441"
inkscape:cx="28.590179"
inkscape:cy="341.52833"
inkscape:window-width="2560"
inkscape:window-height="1371"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="m-logo" />
<path
id="border"
d="m 256,477 c -56.348,0 -98.269,-0.006 -129.636,-3.491 C 94.967,470.02 75.025,463.118 61.953,450.047 48.882,436.975 41.98,417.033 38.491,385.636 35.006,354.269 35,312.348 35,256 35,199.652 35.006,157.731 38.491,126.364 41.98,94.9671 48.882,75.0246 61.953,61.9534 75.025,48.8822 94.967,41.9797 126.364,38.4912 157.731,35.0059 199.652,35 256,35 c 56.348,0 98.269,0.006 129.636,3.4912 31.3969,3.4885 51.3394,10.391 64.4106,23.4622 13.0712,13.0712 19.9737,33.0137 23.4622,64.4106 C 476.9941,157.731 477,199.652 477,256 c 0,56.348 -0.006,98.269 -3.4912,129.636 -3.4885,31.397 -10.391,51.339 -23.4622,64.411 -13.0712,13.071 -33.0137,19.973 -64.4106,23.462 C 354.269,476.994 312.348,477 256,477 Z"
fill="rgba(216, 246, 255, 0)"
stroke="#32a8eb"
stroke-width="8"
stroke-linejoin="round" />
<path
d="M347.694 326.566H360V235.186C360 203.566 340.791 185 310.176 185C290.066 185 270.257 193.413 256.15 208.208C248.046 193.123 232.739 185 212.629 185C194.62 185 177.212 191.382 163.405 202.696L161.004 187.031H152V326.566H164.306V212.269C176.912 201.535 193.12 195.153 209.628 195.153C234.84 195.153 249.847 210.528 249.847 236.927V326.566H262.153V235.186C262.153 227.934 261.253 221.262 259.152 215.46C272.058 202.696 289.466 195.153 307.175 195.153C332.687 195.153 347.694 210.528 347.694 236.927V326.566Z"
stroke="#32a8eb"
fill="rgba(216, 246, 255, 0)"
stroke-width="2"
id="m-letter" />
<path
id="border-start"
d="m 509.27383,509.07982 c -13.78829,-18.55493 -32.05408,-24.58114 -81.31406,-26.9168 -101.10905,-4.7941 -48.5241,-5.4316 -220.91427,-5.25658"
stroke="#32a8eb"
stroke-width="8"
stroke-linejoin="round"
visibility="hidden"
sodipodi:nodetypes="csc" />
<path
d="M 507.76664,507.59022 C 453.73997,452.69049 121.4145,522.45057 289.19959,354.47858 c 10.11721,-10.12848 19.63171,-17.88383 30.25282,-22.95349 6.77433,-3.23351 15.74106,-4.08485 28.19639,-5.03375"
id="m-letter-start"
stroke="#32a8eb"
stroke-width="2"
visibility="hidden"
sodipodi:nodetypes="cssc" />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -14,6 +14,6 @@
</g> </g>
</g> </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="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"/> <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> </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

View File

@ -33,9 +33,9 @@
<em> <a href="https://en.wikipedia.org/wiki/Programmer">programmer</a> </em> <em> <a href="https://en.wikipedia.org/wiki/Programmer">programmer</a> </em>
. I am developing software for more than half of my life and <strong>I love it!</strong> Sometimes I stream working on my side projects and building a <a href="https://discord.gg/2cGg7kwZEh">community of like minded people</a>. Here you can find blogs of my thoughts and journeys, as well as links to my socials where you can see other content.</p> . I am developing software for more than half of my life and <strong>I love it!</strong> Sometimes I stream working on my side projects and building a <a href="https://discord.gg/2cGg7kwZEh">community of like minded people</a>. Here you can find blogs of my thoughts and journeys, as well as links to my socials where you can see other content.</p>
<section id="talent-cards" class="flex flex-col items-center"> <section id="talent-cards" class="my-4 mx-auto flex flex-col items-center gap-4 max-w-[32rem] px-4">
{% call tc::talent_card("code", "Web development", "Extensive expertise in creating performant, live web applications and websites") %} {% call tc::talent_card("code", "Web development", "Extensive expertise in creating performant, live web applications and websites.") %}
{% call tc::talent_card("gamepad", "Game development", "Extensive expertise in creating performant, live web applications and websites") %} {% call tc::talent_card("gamepad", "Game development", "Creating games with passion. Capable of producing each aspect of a good game with profesional sound and creative mechanics. I consider games as multidisciplinary art.") %}
{% call tc::talent_card("person-chalkboard", "Mentoring & Consulting", "I offer consulting sessions to assist you in developing <strong>higher-quality software</strong> and share insights from crafting robust, professional web applications. <a href=\"https://calendly.com/michalvankosk/30min\">Schedule a session with me</a> and elevate your projects together.") %} {% call tc::talent_card("person-chalkboard", "Mentoring & Consulting", "I offer consulting sessions to assist you in developing <strong>higher-quality software</strong> and share insights from crafting robust, professional web applications. <a href=\"https://calendly.com/michalvankosk/30min\">Schedule a session with me</a> and elevate your projects together.") %}
</section> </section>
</section> </section>

205
templates/portfolio.html Normal file
View File

@ -0,0 +1,205 @@
{%- import "components/talent_card.html" as tc -%}
{% extends "base.html" %}
{% block og_meta %}
<meta property="og:title" content="{{title}} @michalvankodev" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://michalvanko.dev/showcase" />
<meta property="og:image" content="https://michalvanko.dev/images/m-logo.svg" />
{% endblock %}
{% block content %}
<header class="max-w-read mx-auto md:grid md:grid-cols-[1fr_auto]">
<div>
<h1 class="px-4 my-2 text-4xl text-blue-950 font-extrabold md:text-6xl xl:text-7xl xl:mb-4 xl:mt-10">Michal Vanko</h1>
<h3 class="px-4 my-1 text-2xl text-blue-900 font-bold italic md:text-3xl xl:text-4xl">Software Architect by passion</h3>
<section id="contact" class="mt-4">
<ul class="max-w-[24rem] mx-auto md:mx-6">
{% for link in contact_links %}
<li class="my-2">
<a
class="flex border-2 place-content-center items-center rounded-full text-blue-900 border-blue-500 py-2 hover:bg-pink-200 fill-blue-900 hover:fill-blue-400 transition-colors no-underline"
href="{{link.href}}"
title="{{link.title}}"
>
<svg aria-hidden="true" class="h-6 w-6 mx-2 self-start">
<use xlink:href="#{{link.svg}}" />
</svg>
<span class="text-lg font-semibold print:hidden">{{link.label}}</span>
<span class="hidden print:inline text-sm">{{link.href}}</span>
</a>
</li>
{% endfor %}
</ul>
</section>
</div>
<aside id="logo-container" class="hidden md:block h-[320px] w-[320px] m-10">
<style>
#logo-container svg {
height: 100%;
width: 100%;
}
</style>
{% include "icons/m-logo-animated.svg" %}
<script src="/resources/anime.min.js"></script>
<script>
var svg = document.getElementById("m-logo")
svg.setAttribute("visibility", "visible");
var borderTimeline = anime.timeline({
duration: 2000,
easing: 'easeInOutSine',
})
borderTimeline.add({
targets: '#m-logo #border-start',
strokeDashoffset: [anime.setDashoffset, -310],
duration: 2000,
easing: 'easeOutExpo',
begin: (animation) => {
const target = animation.animatables[0].target
target.setAttribute("visibility", "visible")
}
}, 0)
borderTimeline.add({
targets: '#m-logo #m-letter-start',
strokeDashoffset: [anime.setDashoffset, -447.4],
easing: 'easeOutExpo',
duration: 2000,
begin: (animation) => {
const target = animation.animatables[0].target
target.setAttribute("visibility", "visible")
}
}, 0)
borderTimeline.add({
targets: '#m-logo #border',
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 2000,
complete: (animation) => {
//debugger;
const target = animation.animatables[0].target
target.setAttribute("fill", "url('#bg-gradient')")
var bgAnimationTimeline = anime.timeline()
bgAnimationTimeline.add({
targets: '#m-logo #bg-gradient #bg-stop',
offset: "0%",
stopColor: "rgba(216, 246, 255, 1)",
easing: 'easeInQuint',
duration: 123,
})
bgAnimationTimeline.add({
targets: '#m-logo #bg-gradient #bg-stop',
offset: "100%",
easing: 'easeOutExpo',
duration: 333,
})
}
}, 160)
borderTimeline.add({
targets: '#m-logo #m-letter',
strokeDashoffset: [anime.setDashoffset, 0],
easing: 'easeInOutSine',
duration: 1800,
}, 160)
borderTimeline.add({
targets: '#m-logo #m-letter',
easing: 'easeInOutSine',
duration: 333,
fill: "#32a8eb",
strokeWidth: "0",
})
// background
/*borderTimeline.add({
targets: '#m-logo #border',
easing: 'easeInCirc',
fill: ["rgba(216, 246, 255, 0)", "rgba(216, 246, 255, 1)"],
duration: 1500,
}, 160)
borderTimeline.add({
targets: '#m-logo #border',
easing: 'easeInCirc',
fill: ["rgba(216, 246, 255, 0)", "rgba(216, 246, 255, 1)"],
duration: 1500,
}, 160) */
</script>
</aside>
</header>
<section id="portfolio-body" class="article-body">
{{ body|parse_markdown|safe }}
</section>
<section id="skillsets" class="max-w-maxindex mx-auto xl:grid xl:gap-8 xl:grid-cols-3">
<section id="work-history" class="max-w-read mx-auto xl:row-span-2">
<h2 class="m-5 text-3xl text-blue-950 font-extrabold md:text-4xl xl:font-medium">Work experience</h2>
<ul class="m-6 grid grid-flow-row gap-6">
{% for skill in workplace_list %}
<li>
{% include "components/skill_card.html" %}
</li>
{% endfor %}
</ul>
</section>
<section id="education" class="max-w-read mx-auto">
<h2 class="m-5 text-3xl text-blue-950 font-extrabold md:text-4xl xl:font-medium">Education</h2>
<ul class="m-6 grid grid-flow-row gap-6">
{% for skill in education_list %}
<li>
{% include "components/skill_card.html" %}
</li>
{% endfor %}
</ul>
</section>
<section id="skills" class="max-w-read mx-auto">
<h2 class="m-5 text-3xl text-blue-950 font-extrabold md:text-4xl xl:font-medium">Skills</h2>
<ul class="m-6 grid grid-flow-row gap-6">
<li>
{% call tc::talent_card("code", "Web development", "Extensive expertise in creating performant, live web applications and websites.",) %}
</li>
<li>
{% call tc::talent_card("gamepad", "Game development", "Creating games with passion. Capable of producing each aspect of a good game with profesional sound and creative mechanics. I consider games as multidisciplinary art.") %}
</li>
<li>
{% call tc::talent_card("person-chalkboard", "Mentoring & Consulting", "I offer consulting sessions to assist you in developing <strong>higher-quality software</strong> and share insights from crafting robust, professional web applications. <a href=\"https://calendly.com/michalvankosk/30min\">Schedule a session with me</a> and elevate your projects together.") %}
</li>
</ul>
</section>
<section id="technologies" class="max-w-read mx-auto xl:col-span-2">
<h2 class="m-5 text-3xl text-blue-950 font-extrabold md:text-4xl xl:font-medium">Technologies</h2>
<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">
{{technology}}
</li>
{% endfor %}
</ul>
</section>
</section>
<section id="project-list-container" class="max-w-maxindex mx-auto">
<section id="project-list">
{% if project_list.len() == 0 %}
<p class="no-posts">You've found void in the space.</p>
{% else %}
<h2 class="m-5 text-3xl text-blue-950 font-extrabold md:text-5xl">
Showcase
</h1>
<ul class="m-6 grid grid-flow-row gap-6 md:grid-cols-2 md:grid-rows-[masonry] md:justify-stretch md:items-stretch xl:grid-cols-3">
{% for project in project_list %}
<li>
{% include "components/project_preview_card.html" %}
</li>
{% endfor %}
</ul>
{% endif %}
</section> <!-- /#project-list -->
</section> <!-- /#project-list-container -->
{% endblock %}

View File

@ -1,6 +1,6 @@
<section class="article-tags text-base"> <section class="article-tags text-base">
{% if tags.len() > 0 %} {% if tags.len() > 0 %}
<ul class="inline"> <ul class="inline" style="view-transition-name: post_tags_{{slug}}">
{% for tag in tags %} {% for tag in tags %}
<li class="inline italic text-blue-700 md:text-lg"> <li class="inline italic text-blue-700 md:text-lg">
<a href="/{{segment}}/tags/{{tag}}">#{{tag}}</a> <a href="/{{segment}}/tags/{{tag}}">#{{tag}}</a>

View File

@ -29,4 +29,9 @@
</section> <!-- /#project-list --> </section> <!-- /#project-list -->
</section> <!-- /#project-list-container --> </section> <!-- /#project-list-container -->
<section class="text-center my-3 md:text-lg">
<a href="/portfolio">see complete portfolio</a>
</section>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
<footer class="my-4"> <footer class="my-4" view-transition-name="site_footer">
<hr class="mb-4 border-slate-300 mx-5"> <hr class="mb-4 border-slate-300 mx-5">
<section class="flex justify-around"> <section class="flex justify-around">
<p <p

View File

@ -1,4 +1,4 @@
<header class="min-h-full bg-blue-50 mb-5"> <header class="min-h-full bg-blue-50 mb-5" view-transition-name="site_header">
<nav class="flex"> <nav class="flex">
{% match header_props.back_link %} {% match header_props.back_link %}
{% when Some with (link) %} {% when Some with (link) %}

21
test-animate.svg Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100%" height="100%"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Your original SVG code here -->
<path id="path1" d="M10 50 L20 50"></path>
<path id="path2" d="M30 50 C40 70, 60 90, 80 110"></path>
<!-- The animation elements -->
<animateMotion
path="M10 50 S20 50 20 50"
dur="2000ms"
repeatCount="indefinite">
</animateMotion>
<animateMotion
path="M30 50 S40 70, 60 90, 80 110"
begin="2s"
dur="2000ms"
repeatCount="indefinite">
</animateMotion>
</svg>

After

Width:  |  Height:  |  Size: 569 B