Compare commits
12 Commits
rust-rewor
...
2e47c91985
Author | SHA1 | Date | |
---|---|---|---|
2e47c91985 | |||
08050baf98 | |||
e04e4f6491 | |||
2a043ae823 | |||
ceb3f4b89d | |||
2979e21285 | |||
4f09373df3 | |||
8c72e7b440 | |||
49f830cd44 | |||
da78b80587 | |||
1ce8ccfdd5 | |||
f42ebc7044 |
@ -1,27 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'prettier',
|
|
||||||
],
|
|
||||||
plugins: ['svelte3', '@typescript-eslint'],
|
|
||||||
ignorePatterns: ['*.cjs'],
|
|
||||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
|
||||||
settings: {
|
|
||||||
'svelte3/typescript': require('typescript'),
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaVersion: 2019,
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2017: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
|
||||||
},
|
|
||||||
}
|
|
41
.gitignore
vendored
41
.gitignore
vendored
@ -1,26 +1,25 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/node_modules/
|
|
||||||
/src/node_modules/@sapper/
|
|
||||||
yarn-error.log
|
|
||||||
/cypress/screenshots/
|
|
||||||
/__sapper__/
|
|
||||||
|
|
||||||
/.svelte-kit
|
# `dist` folder with the export of SSG
|
||||||
/.svelte/
|
|
||||||
/build/
|
|
||||||
/functions/
|
|
||||||
/static/build/
|
|
||||||
|
|
||||||
#amplify
|
|
||||||
amplify/\#current-cloud-backend
|
|
||||||
amplify/.config/local-*
|
|
||||||
amplify/backend/amplify-meta.json
|
|
||||||
amplify/backend/awscloudformation
|
|
||||||
dist/
|
dist/
|
||||||
node_modules/
|
|
||||||
aws-exports.js
|
|
||||||
awsconfiguration.json
|
|
||||||
|
|
||||||
/static/**/optimized/
|
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
.netlify
|
.netlify
|
||||||
|
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# Image generator
|
||||||
|
generated_images/
|
||||||
|
0
.vscode/settings.json
vendored
0
.vscode/settings.json
vendored
31
Caddyfile-preview
Normal file
31
Caddyfile-preview
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
:3081 {
|
||||||
|
root * dist
|
||||||
|
|
||||||
|
# Match requests with trailing slashes and rewrite them
|
||||||
|
@trailing_slash {
|
||||||
|
path_regexp strip_slash ^(.+)/$ # Match paths ending with /
|
||||||
|
}
|
||||||
|
rewrite @trailing_slash {re.strip_slash.1} # Remove the trailing slash
|
||||||
|
|
||||||
|
try_files {path}.html {path}/index.html {path}
|
||||||
|
encode zstd gzip
|
||||||
|
file_server
|
||||||
|
|
||||||
|
# Cache images, videos, fonts, etc. for 1 year (365 days)
|
||||||
|
@static_assets {
|
||||||
|
path_regexp static_assets \.(jpg|jpeg|png|gif|svg|ico|mp4|webm|woff|woff2|eot|ttf|otf|js)$
|
||||||
|
}
|
||||||
|
header @static_assets Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
# Optionally, you can set a fallback for other files
|
||||||
|
header ?Cache-Control "public, max-age=3600"
|
||||||
|
|
||||||
|
handle_errors {
|
||||||
|
@404 {
|
||||||
|
expression {http.error.status_code} == 404
|
||||||
|
}
|
||||||
|
rewrite @404 /not-found.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ 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:
|
||||||
|
@ -28,7 +28,7 @@ This week I've attended a [Rusty game jam #2](https://itch.io/jam/rusty-jam-2).
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
[You can check the result built with WASM here.](/showcase/egg-fetcher/)
|
[You can check the result built with WASM here.](/showcase/egg-fetcher)
|
||||||
|
|
||||||
## What's up with the weeklys
|
## What's up with the weeklys
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ layout: blog
|
|||||||
title: "DevBreak #5 Joys and concerns of Engineering manager with Pavol Dudrík"
|
title: "DevBreak #5 Joys and concerns of Engineering manager with Pavol Dudrík"
|
||||||
segments:
|
segments:
|
||||||
- broadcasts
|
- broadcasts
|
||||||
|
- featured
|
||||||
published: true
|
published: true
|
||||||
date: 2023-02-04T20:22:21.191Z
|
date: 2023-02-04T20:22:21.191Z
|
||||||
thumbnail: /images/uploads/devbreak.jpeg
|
thumbnail: /images/uploads/devbreak.jpeg
|
||||||
|
18
appveyor.yml
18
appveyor.yml
@ -1,18 +0,0 @@
|
|||||||
version: "{build}"
|
|
||||||
|
|
||||||
shallow_clone: true
|
|
||||||
|
|
||||||
init:
|
|
||||||
- git config --global core.autocrlf false
|
|
||||||
|
|
||||||
build: off
|
|
||||||
|
|
||||||
environment:
|
|
||||||
matrix:
|
|
||||||
# node.js
|
|
||||||
- nodejs_version: stable
|
|
||||||
|
|
||||||
install:
|
|
||||||
- ps: Install-Product node $env:nodejs_version
|
|
||||||
- npm install cypress
|
|
||||||
- npm install
|
|
20
axum_server/.gitignore
vendored
20
axum_server/.gitignore
vendored
@ -1,20 +0,0 @@
|
|||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
|
||||||
*.pdb
|
|
||||||
|
|
||||||
# `dist` folder with the export of SSG
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Image generator
|
|
||||||
generated_images/
|
|
@ -1,15 +0,0 @@
|
|||||||
use axum::http::StatusCode;
|
|
||||||
|
|
||||||
use crate::post_utils::{post_listing::get_post_list, post_parser::ParseResult};
|
|
||||||
|
|
||||||
use super::blog_post_model::{BlogPostMetadata, BLOG_POST_PATH};
|
|
||||||
|
|
||||||
pub async fn get_featured_blog_posts() -> Result<Vec<ParseResult<BlogPostMetadata>>, StatusCode> {
|
|
||||||
let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH).await?;
|
|
||||||
post_list.retain(|post| post.metadata.segments.contains(&"featured".to_string()));
|
|
||||||
post_list.retain(|post| post.metadata.published);
|
|
||||||
post_list.sort_by_key(|post| post.metadata.date);
|
|
||||||
post_list.reverse();
|
|
||||||
|
|
||||||
Ok(post_list)
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
pub mod blog_post_model;
|
|
||||||
pub mod featured_blog_posts;
|
|
||||||
pub mod tag_list;
|
|
@ -1,38 +0,0 @@
|
|||||||
use axum::http::StatusCode;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
blog_posts::blog_post_model::{BlogPostMetadata, BLOG_POST_PATH},
|
|
||||||
post_utils::post_listing::get_post_list,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn get_popular_blog_tags() -> Result<Vec<String>, StatusCode> {
|
|
||||||
const TAGS_LENGTH: usize = 7;
|
|
||||||
|
|
||||||
let post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH).await?;
|
|
||||||
let tags_sum = post_list
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|post| post.metadata.tags)
|
|
||||||
.fold(HashMap::new(), |mut acc, tag| {
|
|
||||||
*acc.entry(tag).or_insert(0) += 1;
|
|
||||||
acc
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut sorted_tags_by_count: Vec<_> = tags_sum.into_iter().collect();
|
|
||||||
sorted_tags_by_count.sort_by_key(|&(_, count)| std::cmp::Reverse(count));
|
|
||||||
|
|
||||||
// Log the counts
|
|
||||||
for (tag, count) in &sorted_tags_by_count {
|
|
||||||
debug!("Tag: {}, Count: {}", tag, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
let popular_blog_tags = sorted_tags_by_count
|
|
||||||
.into_iter()
|
|
||||||
.map(|tag_count| tag_count.0)
|
|
||||||
.filter(|tag| tag != "dev")
|
|
||||||
.take(TAGS_LENGTH)
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
|
|
||||||
Ok(popular_blog_tags)
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
use askama::Template;
|
|
||||||
use axum::{extract::Path, http::StatusCode};
|
|
||||||
use tokio::try_join;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
blog_posts::{
|
|
||||||
blog_post_model::{BlogPostMetadata, BLOG_POST_PATH},
|
|
||||||
tag_list::get_popular_blog_tags,
|
|
||||||
},
|
|
||||||
components::site_header::{HeaderProps, Link},
|
|
||||||
filters,
|
|
||||||
post_utils::{post_listing::get_post_list, post_parser::ParseResult},
|
|
||||||
projects::{featured_projects::get_featured_projects, project_model::ProjectMetadata},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "blog_post_list.html")]
|
|
||||||
pub struct PostListTemplate {
|
|
||||||
pub title: String,
|
|
||||||
pub posts: Vec<ParseResult<BlogPostMetadata>>,
|
|
||||||
pub tag: Option<String>,
|
|
||||||
pub header_props: HeaderProps,
|
|
||||||
pub blog_tags: Vec<String>,
|
|
||||||
pub featured_projects: Vec<ParseResult<ProjectMetadata>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render_blog_post_list(
|
|
||||||
tag: Option<Path<String>>,
|
|
||||||
) -> Result<PostListTemplate, StatusCode> {
|
|
||||||
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
|
||||||
let tag = tag.map(|Path(tag)| tag);
|
|
||||||
|
|
||||||
let (blog_tags, featured_projects, mut post_list) = try_join!(
|
|
||||||
get_popular_blog_tags(),
|
|
||||||
get_featured_projects(),
|
|
||||||
get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
post_list.sort_by_key(|post| post.metadata.date);
|
|
||||||
post_list.retain(|post| post.metadata.published);
|
|
||||||
post_list.reverse();
|
|
||||||
|
|
||||||
let posts = match &tag {
|
|
||||||
Some(tag) => post_list
|
|
||||||
.into_iter()
|
|
||||||
.filter(|post| {
|
|
||||||
post.metadata
|
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.map(|post_tag| post_tag.to_lowercase())
|
|
||||||
.collect::<String>()
|
|
||||||
.contains(&tag.to_lowercase())
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
None => post_list,
|
|
||||||
};
|
|
||||||
|
|
||||||
let header_props = match tag {
|
|
||||||
Some(_) => HeaderProps::with_back_link(Link {
|
|
||||||
href: "/blog".to_string(),
|
|
||||||
label: "All posts".to_string(),
|
|
||||||
}),
|
|
||||||
None => HeaderProps::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(PostListTemplate {
|
|
||||||
title: "Blog posts".to_owned(),
|
|
||||||
posts,
|
|
||||||
tag,
|
|
||||||
header_props,
|
|
||||||
blog_tags,
|
|
||||||
featured_projects,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
use askama::Template;
|
|
||||||
use axum::{extract::Path, http::StatusCode};
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
blog_posts::blog_post_model::BlogPostMetadata, components::site_header::Link, filters,
|
|
||||||
post_utils::post_parser::parse_post,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::components::site_header::HeaderProps;
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "blog_post.html")]
|
|
||||||
pub struct BlogPostTemplate {
|
|
||||||
pub title: String,
|
|
||||||
pub body: String,
|
|
||||||
pub date: DateTime<Utc>,
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
pub header_props: HeaderProps,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render_blog_post(Path(post_id): Path<String>) -> Result<BlogPostTemplate, StatusCode> {
|
|
||||||
let path = format!("../_posts/blog/{}.md", post_id);
|
|
||||||
let parse_post = parse_post::<BlogPostMetadata>(&path, true);
|
|
||||||
let parsed = parse_post.await?;
|
|
||||||
|
|
||||||
Ok(BlogPostTemplate {
|
|
||||||
title: parsed.metadata.title,
|
|
||||||
date: parsed.metadata.date,
|
|
||||||
tags: parsed.metadata.tags,
|
|
||||||
body: parsed.body,
|
|
||||||
header_props: HeaderProps::with_back_link(Link {
|
|
||||||
href: "/blog".to_string(),
|
|
||||||
label: "All posts".to_string(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
use askama::Template;
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use tokio::try_join;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
blog_posts::{
|
|
||||||
blog_post_model::BlogPostMetadata, featured_blog_posts::get_featured_blog_posts,
|
|
||||||
tag_list::get_popular_blog_tags,
|
|
||||||
},
|
|
||||||
components::site_header::HeaderProps,
|
|
||||||
filters,
|
|
||||||
post_utils::post_parser::ParseResult,
|
|
||||||
projects::{featured_projects::get_featured_projects, project_model::ProjectMetadata},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "index.html")]
|
|
||||||
pub struct IndexTemplate {
|
|
||||||
header_props: HeaderProps,
|
|
||||||
blog_tags: Vec<String>,
|
|
||||||
featured_blog_posts: Vec<ParseResult<BlogPostMetadata>>,
|
|
||||||
featured_projects: Vec<ParseResult<ProjectMetadata>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
|
||||||
let (blog_tags, featured_blog_posts, featured_projects) = try_join!(
|
|
||||||
get_popular_blog_tags(),
|
|
||||||
get_featured_blog_posts(),
|
|
||||||
get_featured_projects()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(IndexTemplate {
|
|
||||||
header_props: HeaderProps::default(),
|
|
||||||
blog_tags,
|
|
||||||
featured_blog_posts,
|
|
||||||
featured_projects,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:3000",
|
|
||||||
"video": false
|
|
||||||
}
|
|
@ -5,18 +5,15 @@ tailwind:
|
|||||||
npx tailwindcss -i ./styles/input.css -o ./styles/output.css --watch
|
npx tailwindcss -i ./styles/input.css -o ./styles/output.css --watch
|
||||||
|
|
||||||
# svg sprite creation
|
# svg sprite creation
|
||||||
# TODO change route on svetle project deletion
|
|
||||||
# TODO #directory-swap
|
|
||||||
svgstore:
|
svgstore:
|
||||||
npx svgstore -o ../static/svg/icons-sprite.svg ../src/svg/**.svg
|
npx svgstore -o templates/icons/sprite.svg src/svg/**.svg
|
||||||
|
|
||||||
server_dev:
|
server_dev:
|
||||||
cargo watch -x run
|
cargo watch -x run
|
||||||
|
|
||||||
# CMS server for local dev
|
# CMS server for local dev
|
||||||
# TODO #directory-swap
|
|
||||||
decap_server:
|
decap_server:
|
||||||
cd .. && npx decap-server
|
npx decap-server
|
||||||
|
|
||||||
# Run dev server in watch mode
|
# Run dev server in watch mode
|
||||||
dev:
|
dev:
|
||||||
@ -33,7 +30,7 @@ test_watch:
|
|||||||
cargo watch -x test
|
cargo watch -x test
|
||||||
|
|
||||||
# Run server in production mode
|
# Run server in production mode
|
||||||
prod:
|
prod $TARGET="PROD" $RUST_LOG="info":
|
||||||
cargo run --release
|
cargo run --release
|
||||||
|
|
||||||
# Wait for port to listen to connections
|
# Wait for port to listen to connections
|
||||||
@ -55,11 +52,12 @@ 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 -P dist/svg 127.0.0.1:{{port}}/svg/icons-sprite.svg
|
- wget --no-convert-links -p -E -P dist --no-host-directories 127.0.0.1:{{port}}/not-found
|
||||||
|
find generated_images/ -name "*_og*" -exec cp --parents {} dist/ \;
|
||||||
|
|
||||||
# Preview server
|
# Preview server
|
||||||
preview:
|
preview:
|
||||||
npx http-server dist
|
caddy run --config Caddyfile-preview
|
||||||
|
|
||||||
# SSG export of production server
|
# SSG export of production server
|
||||||
export: clean
|
export: clean
|
7319
package-lock.json
generated
7319
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@ -1,56 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "michalvankodev",
|
|
||||||
"description": "My personal website with blog",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite dev",
|
|
||||||
"prebuild": "npm run svgstore",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"start": "svelte-kit start",
|
|
||||||
"test": "vitest",
|
|
||||||
"svgstore": "svgstore -o src/svg/build/icons-sprite.svg src/svg/**.svg",
|
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
||||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
|
||||||
"format": "prettier --plugin-search-dir . --write ."
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@vanilla-extract/css": "^1.9.2",
|
|
||||||
"@vanilla-extract/sprinkles": "^1.5.1",
|
|
||||||
"@vanilla-extract/vite-plugin": "^3.7.0",
|
|
||||||
"classnames": "^2.3.2",
|
|
||||||
"date-fns": "^2.29.3",
|
|
||||||
"feed": "^4.2.2",
|
|
||||||
"front-matter": "^4.0.2",
|
|
||||||
"marked": "^3.0.8",
|
|
||||||
"modern-normalize": "^1.1.0",
|
|
||||||
"polished": "^4.2.2",
|
|
||||||
"prismjs": "^1.29.0",
|
|
||||||
"ramda": "^0.28.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@sveltejs/adapter-static": "^1.0.4",
|
|
||||||
"@sveltejs/kit": "^1.1.1",
|
|
||||||
"@tsconfig/svelte": "^3.0.0",
|
|
||||||
"@types/classnames": "^2.3.1",
|
|
||||||
"@types/node": "^18.11.18",
|
|
||||||
"@types/ramda": "^0.28.21",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
|
||||||
"@typescript-eslint/parser": "^5.48.2",
|
|
||||||
"eslint": "^8.32.0",
|
|
||||||
"eslint-config-prettier": "^8.6.0",
|
|
||||||
"eslint-plugin-svelte3": "^4.0.0",
|
|
||||||
"prettier": "~2.8.3",
|
|
||||||
"prettier-plugin-svelte": "^2.9.0",
|
|
||||||
"svelte": "^3.55.1",
|
|
||||||
"svelte-preprocess": "^5.0.0",
|
|
||||||
"svgstore-cli": "^2.0.1",
|
|
||||||
"tslib": "^2.4.1",
|
|
||||||
"typescript": "^4.9.4",
|
|
||||||
"vite": "^4.0.4",
|
|
||||||
"vitest": "^0.27.2",
|
|
||||||
"vitest-svelte-kit": "^0.0.6"
|
|
||||||
}
|
|
||||||
}
|
|
2
robots.txt
Normal file
2
robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /admin
|
31
src/app.html
31
src/app.html
@ -1,31 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
||||||
<meta name="theme-color" content="#333333" />
|
|
||||||
|
|
||||||
<meta name="description" content="Personal website of @michalvankodev" />
|
|
||||||
<meta name="keywords" content="personal, blog, webdev, tech, programming" />
|
|
||||||
<meta name="robots" content="index, follow" />
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS feed for latest posts" href="https://michalvanko.dev/feed.xml" />
|
|
||||||
<link rel="alternate" title="JSON feed for latest posts" type="application/json" href="https://michalvanko.dev/feed.json" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/print.css" media="print" />
|
|
||||||
<link rel="stylesheet" href="/fonts.css" />
|
|
||||||
<link rel="manifest" href="/manifest.json" />
|
|
||||||
<link rel="stylesheet" href="/prism.css" />
|
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/m-logo.svg" />
|
|
||||||
<link rel="icon" type="image/png" href="/m-logo-192.png" />
|
|
||||||
<!-- This contains the contents of the <svelte:head> component, if
|
|
||||||
the current page has one -->
|
|
||||||
%sveltekit.head%
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div style="display: contents">
|
|
||||||
%sveltekit.body%
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,14 +1,23 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::post_utils::post_parser::deserialize_date;
|
use crate::post_utils::post_parser::deserialize_date;
|
||||||
|
|
||||||
pub const BLOG_POST_PATH: &str = "../_posts/blog";
|
pub const BLOG_POST_PATH: &str = "_posts/blog";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "lowercase")] // Optional, this converts enum variants to lowercase
|
||||||
|
pub enum Segment {
|
||||||
|
Blog,
|
||||||
|
Broadcasts,
|
||||||
|
Featured,
|
||||||
|
Cookbook,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct BlogPostMetadata {
|
pub struct BlogPostMetadata {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub segments: Vec<String>,
|
pub segments: Vec<Segment>,
|
||||||
pub published: bool,
|
pub published: bool,
|
||||||
#[serde(deserialize_with = "deserialize_date")]
|
#[serde(deserialize_with = "deserialize_date")]
|
||||||
pub date: DateTime<Utc>,
|
pub date: DateTime<Utc>,
|
1
src/blog_posts/mod.rs
Normal file
1
src/blog_posts/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod blog_post_model;
|
@ -49,5 +49,5 @@ pub async fn render_rss_feed() -> Result<impl IntoResponse, StatusCode> {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
let response = feed_builder.to_string();
|
let response = feed_builder.to_string();
|
||||||
return Ok(([(header::CONTENT_TYPE, "application/xml")], response));
|
Ok(([(header::CONTENT_TYPE, "application/xml")], response))
|
||||||
}
|
}
|
3
src/global.d.ts
vendored
3
src/global.d.ts
vendored
@ -1,3 +0,0 @@
|
|||||||
/// <reference types="@sveltejs/kit" />
|
|
||||||
/// <reference types="svelte" />
|
|
||||||
/// <reference types="vite/client" />
|
|
@ -1,38 +0,0 @@
|
|||||||
import { error } from '@sveltejs/kit'
|
|
||||||
import fm from 'front-matter'
|
|
||||||
import { readFile } from 'fs'
|
|
||||||
import { parseField } from '$lib/markdown/parse-markdown'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
|
|
||||||
export interface ArticleAttributes {
|
|
||||||
slug: string
|
|
||||||
layout: string
|
|
||||||
segments: string[]
|
|
||||||
title: string
|
|
||||||
published: boolean
|
|
||||||
date: string
|
|
||||||
thumbnail: string
|
|
||||||
tags: string[]
|
|
||||||
body: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getArticleContent(slug: string) {
|
|
||||||
let postSource: string
|
|
||||||
try {
|
|
||||||
postSource = await promisify(readFile)(`_posts/blog/${slug}.md`, 'utf-8')
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.code === 'ENOENT') {
|
|
||||||
throw error(404, 'Post not found \n' + e.toString())
|
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedPost = fm<ArticleAttributes>(postSource)
|
|
||||||
|
|
||||||
const post = parseField<ArticleAttributes>('body')({
|
|
||||||
...parsedPost.attributes,
|
|
||||||
body: parsedPost.body,
|
|
||||||
})
|
|
||||||
return post
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
import { readdir, readFile } from 'fs'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { basename } from 'path'
|
|
||||||
import { pipe, prop, sortBy, reverse, filter } from 'ramda'
|
|
||||||
import fm from 'front-matter'
|
|
||||||
import marked from 'marked'
|
|
||||||
import {
|
|
||||||
filterAndCount,
|
|
||||||
type PaginationQuery,
|
|
||||||
} from '$lib/pagination/pagination'
|
|
||||||
import type { ArticleAttributes } from './articleContent'
|
|
||||||
|
|
||||||
export interface ArticlePreviewAttributes extends ArticleAttributes {
|
|
||||||
preview: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const { NODE_ENV } = process.env
|
|
||||||
export async function getBlogListing(paginationQuery: PaginationQuery) {
|
|
||||||
const files = await promisify(readdir)(`_posts/blog/`, 'utf-8')
|
|
||||||
const filteredFiles = filterDevelopmentFiles(files)
|
|
||||||
|
|
||||||
const contents = await Promise.all(
|
|
||||||
filteredFiles.map(async (file) => {
|
|
||||||
const fileContent = await promisify(readFile)(
|
|
||||||
`_posts/blog/${file}`,
|
|
||||||
'utf-8'
|
|
||||||
)
|
|
||||||
const parsedAttributes = fm<ArticleAttributes>(fileContent)
|
|
||||||
|
|
||||||
const lineOfTextRegExp = /^(?:\w|\[).+/gm
|
|
||||||
const lines = parsedAttributes.body
|
|
||||||
.match(lineOfTextRegExp)
|
|
||||||
.slice(0, 2)
|
|
||||||
.join('\n')
|
|
||||||
|
|
||||||
const preview = marked(lines)
|
|
||||||
return {
|
|
||||||
...parsedAttributes.attributes,
|
|
||||||
preview,
|
|
||||||
slug: basename(file, '.md'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const filteredContents = pipe(
|
|
||||||
sortBy<ArticlePreviewAttributes>(prop('date')),
|
|
||||||
(items) => reverse(items),
|
|
||||||
filter<(typeof contents)[0]>((article) => article.published),
|
|
||||||
filterAndCount(paginationQuery)
|
|
||||||
)(contents)
|
|
||||||
|
|
||||||
return filteredContents
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterDevelopmentFiles(files: string[]) {
|
|
||||||
return NODE_ENV !== 'production'
|
|
||||||
? files
|
|
||||||
: files.filter((file) => !file.startsWith('dev-'))
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
onMountScripts?: Array<() => void>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export function runOnMountScripts() {
|
|
||||||
window.onMountScripts?.forEach((fn) => {
|
|
||||||
fn()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
|||||||
import { globalStyle, style } from '@vanilla-extract/css'
|
|
||||||
import { radialGradient, rgba, transparentize } from 'polished'
|
|
||||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
|
||||||
import {
|
|
||||||
breakpoints,
|
|
||||||
colors,
|
|
||||||
mediaAt,
|
|
||||||
menuBackground,
|
|
||||||
transparent,
|
|
||||||
vars,
|
|
||||||
} from '$lib/styles/vars.css'
|
|
||||||
|
|
||||||
export const siteFooterClass = style([
|
|
||||||
sprinkles({
|
|
||||||
fontSize: { mobile: 'base', desktop: 'sm' },
|
|
||||||
paddingX: '2x',
|
|
||||||
paddingTop: '1x',
|
|
||||||
color: 'menuLink',
|
|
||||||
}),
|
|
||||||
|
|
||||||
radialGradient({
|
|
||||||
colorStops: [
|
|
||||||
`${menuBackground} 56%`,
|
|
||||||
`${transparentize(0.4, menuBackground)} 100%`,
|
|
||||||
],
|
|
||||||
extent: '160% 100% at 100% 100%',
|
|
||||||
fallback: transparent,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
'@media': {
|
|
||||||
[mediaAt(breakpoints.m)]: radialGradient({
|
|
||||||
colorStops: [
|
|
||||||
`${menuBackground} 48%`,
|
|
||||||
`${transparentize(1, menuBackground)} 100%`,
|
|
||||||
],
|
|
||||||
extent: '140% 100% at 100% 100%',
|
|
||||||
fallback: transparent,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
export const headerClass = sprinkles({
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 'base',
|
|
||||||
color: 'menuLink',
|
|
||||||
margin: 'none',
|
|
||||||
lineHeight: '3x',
|
|
||||||
marginBottom: '1x',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const sectionListsClass = style([
|
|
||||||
sprinkles({
|
|
||||||
display: 'grid',
|
|
||||||
justifyItems: { mobile: 'center', desktop: 'start' },
|
|
||||||
textAlign: { mobile: 'center', desktop: 'start' },
|
|
||||||
maxWidth: 'max',
|
|
||||||
columnGap: '3x',
|
|
||||||
margin: 'auto',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
'@media': {
|
|
||||||
[mediaAt(breakpoints.l)]: {
|
|
||||||
gridTemplateColumns: 'auto auto auto',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
export const sectionListSectionClass = sprinkles({
|
|
||||||
marginY: '3x',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const listUlClass = sprinkles({
|
|
||||||
listStyle: 'none',
|
|
||||||
padding: 'none',
|
|
||||||
margin: 'none',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const listLiClass = sprinkles({
|
|
||||||
marginLeft: '1x',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const nestedListLiClass = style([
|
|
||||||
listLiClass,
|
|
||||||
sprinkles({
|
|
||||||
fontSize: 'sm',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
export const socialLinkLabelClass = sprinkles({
|
|
||||||
paddingX: '1x',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const svgClass = style({
|
|
||||||
fill: vars.color.menuLink,
|
|
||||||
height: '1em',
|
|
||||||
width: '1em',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const strokeSvgClass = style([
|
|
||||||
svgClass,
|
|
||||||
{
|
|
||||||
stroke: vars.color.menuLink,
|
|
||||||
strokeWidth: '2px',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
export const socialLinkClass = sprinkles({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: {
|
|
||||||
mobile: 'center',
|
|
||||||
desktop: 'start',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const bottomLineClass = sprinkles({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginX: 'auto',
|
|
||||||
paddingBottom: '1x',
|
|
||||||
marginTop: '2x',
|
|
||||||
maxWidth: 'max',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const dateClass = sprinkles({
|
|
||||||
fontSize: 'xs',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const boldClass = sprinkles({
|
|
||||||
fontWeight: 'bold',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const hrClass = style([
|
|
||||||
sprinkles({
|
|
||||||
marginY: '2x',
|
|
||||||
marginX: '1x',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
color: rgba(colors.midnightBlue, 0.14),
|
|
||||||
borderWidth: '1px 0 0',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
export const licenceText = sprinkles({
|
|
||||||
textAlign: 'center',
|
|
||||||
width: 'parent',
|
|
||||||
fontSize: 'xs',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const latestPostsClass = style({})
|
|
||||||
|
|
||||||
globalStyle(`${siteFooterClass} a`, {
|
|
||||||
color: vars.color.menuLink,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${headerClass} a:link, ${headerClass} a:visited`, {
|
|
||||||
color: vars.color.menuLink,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${siteFooterClass} a:hover`, {
|
|
||||||
color: vars.color.menuLinkHover,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${latestPostsClass} li a:visited:not(:hover)`, {
|
|
||||||
color: vars.color.linkVisited,
|
|
||||||
})
|
|
@ -1,199 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { format } from 'date-fns'
|
|
||||||
import type { ArticlePreviewAttributes } from '$lib/articleContent/articleContentListing'
|
|
||||||
import SvgIcon from './SvgIcon.svelte'
|
|
||||||
import {
|
|
||||||
boldClass,
|
|
||||||
bottomLineClass,
|
|
||||||
dateClass,
|
|
||||||
headerClass,
|
|
||||||
hrClass,
|
|
||||||
latestPostsClass,
|
|
||||||
listLiClass,
|
|
||||||
listUlClass,
|
|
||||||
nestedListLiClass,
|
|
||||||
sectionListsClass,
|
|
||||||
sectionListSectionClass,
|
|
||||||
siteFooterClass,
|
|
||||||
socialLinkClass,
|
|
||||||
socialLinkLabelClass,
|
|
||||||
strokeSvgClass,
|
|
||||||
svgClass,
|
|
||||||
licenceText,
|
|
||||||
} from './Footer.css'
|
|
||||||
|
|
||||||
export let latestPosts: ArticlePreviewAttributes[]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<footer class="site-footer navigation-theme {siteFooterClass}">
|
|
||||||
<div class="lists {sectionListsClass}">
|
|
||||||
<section class="site-map {sectionListSectionClass}">
|
|
||||||
<ul class={listUlClass}>
|
|
||||||
<li class={listLiClass}>
|
|
||||||
<a href="/">Introduction</a>
|
|
||||||
</li>
|
|
||||||
<li class={listLiClass}>
|
|
||||||
<a href="/portfolio">Portfolio</a>
|
|
||||||
<ul class={listUlClass}>
|
|
||||||
<li class={nestedListLiClass}>
|
|
||||||
<a href="/portfolio#personal-information">About</a>
|
|
||||||
</li>
|
|
||||||
<li class={nestedListLiClass}>
|
|
||||||
<a href="/portfolio#skills">Skills</a>
|
|
||||||
</li>
|
|
||||||
<li class={nestedListLiClass}>
|
|
||||||
<a href="/portfolio#work-history">Work History</a>
|
|
||||||
</li>
|
|
||||||
<li class={nestedListLiClass}>
|
|
||||||
<a href="/portfolio#projects">Projects</a>
|
|
||||||
</li>
|
|
||||||
<li class={nestedListLiClass}>
|
|
||||||
<a href="/portfolio#education">Education</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section class="latest-posts {sectionListSectionClass} {latestPostsClass}">
|
|
||||||
<h3 class={headerClass}>
|
|
||||||
<a href="/blog">Latest posts</a>
|
|
||||||
</h3>
|
|
||||||
<ul class={listUlClass}>
|
|
||||||
{#each latestPosts as post}
|
|
||||||
<li class={listLiClass}>
|
|
||||||
<a rel="prefetch" href="/{post.segments[0]}/{post.slug}">
|
|
||||||
<span>{post.title}</span>
|
|
||||||
<time class="date {dateClass}" datetime={post.date}>
|
|
||||||
- {format(new Date(post.date), 'do MMM, yyyy')}
|
|
||||||
</time>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
<hr class={hrClass} />
|
|
||||||
<section class="subscribe {boldClass}">
|
|
||||||
<a href="/feed.xml" rel="external" title="RSS feed" class="rss">
|
|
||||||
Subscribe
|
|
||||||
<SvgIcon name="rss" className={svgClass} />
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/feed.json"
|
|
||||||
rel="external"
|
|
||||||
title="JSON feed"
|
|
||||||
class="json-feed"
|
|
||||||
aria-label="Subscribe with JSON feed"
|
|
||||||
>
|
|
||||||
<SvgIcon name="json-feed" className={svgClass} />
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
<section class="socials {sectionListSectionClass}">
|
|
||||||
<h3 class={headerClass}>Contact</h3>
|
|
||||||
<ul class="social-links {listUlClass}">
|
|
||||||
<li class="email {listLiClass}">
|
|
||||||
<a
|
|
||||||
class={socialLinkClass}
|
|
||||||
href="mailto: michalvankosk@gmail.com"
|
|
||||||
title="E-mail address"
|
|
||||||
>
|
|
||||||
<SvgIcon name="mail" className={svgClass} />
|
|
||||||
<span class={socialLinkLabelClass}>michalvankosk@gmail.com</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="twitter {listLiClass}">
|
|
||||||
<a
|
|
||||||
class={socialLinkClass}
|
|
||||||
href="https://twitter.com/michalvankodev"
|
|
||||||
title="Twitter profile"
|
|
||||||
>
|
|
||||||
<SvgIcon name="twitter" className={strokeSvgClass} />
|
|
||||||
<span class={socialLinkLabelClass}>Twitter</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="github {listLiClass}">
|
|
||||||
<a
|
|
||||||
class={socialLinkClass}
|
|
||||||
href="https://github.com/michalvankodev"
|
|
||||||
title="Github profile"
|
|
||||||
>
|
|
||||||
<SvgIcon name="github" className={strokeSvgClass} />
|
|
||||||
<span class={socialLinkLabelClass}>Github</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="linkedin {listLiClass}">
|
|
||||||
<a
|
|
||||||
class={socialLinkClass}
|
|
||||||
href="https://www.linkedin.com/in/michal-vanko-dev/"
|
|
||||||
title="LinkedIn profile"
|
|
||||||
>
|
|
||||||
<SvgIcon name="linkedin" className={svgClass} />
|
|
||||||
<span class={socialLinkLabelClass}>LinkedIn</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="twitch {listLiClass}">
|
|
||||||
<a
|
|
||||||
class={socialLinkClass}
|
|
||||||
href="https://twitch.tv/michalvankodev"
|
|
||||||
title="Twitch profile"
|
|
||||||
>
|
|
||||||
<SvgIcon name="twitch" className={svgClass} />
|
|
||||||
<span class={socialLinkLabelClass}>Twitch</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="instagram {listLiClass}">
|
|
||||||
<a
|
|
||||||
class={socialLinkClass}
|
|
||||||
href="https://www.instagram.com/michalvankodev/"
|
|
||||||
title="Instagram profile"
|
|
||||||
>
|
|
||||||
<SvgIcon name="instagram" className={svgClass} />
|
|
||||||
<span class={socialLinkLabelClass}>Instagram</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<footer class={bottomLineClass}>
|
|
||||||
<p
|
|
||||||
class={licenceText}
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:dct="http://purl.org/dc/terms/"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
property="dct:title"
|
|
||||||
rel="cc:attributionURL"
|
|
||||||
href="https://michalvanko.dev/">michalvanko.dev</a
|
|
||||||
>
|
|
||||||
by
|
|
||||||
<a
|
|
||||||
rel="cc:attributionURL dct:creator"
|
|
||||||
property="cc:attributionName"
|
|
||||||
href="https://michalvanko.dev/">Michal Vanko</a
|
|
||||||
>
|
|
||||||
is licensed under
|
|
||||||
<a
|
|
||||||
href="http://creativecommons.org/licenses/by-nc-nd/4.0/?ref=chooser-v1"
|
|
||||||
target="_blank"
|
|
||||||
rel="license noopener noreferrer"
|
|
||||||
style="display:inline-block;"
|
|
||||||
>CC BY-NC-ND 4.0<img
|
|
||||||
style="height:22px!important;margin-left:3px;vertical-align:text-bottom;"
|
|
||||||
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"
|
|
||||||
alt="cc"
|
|
||||||
/><img
|
|
||||||
style="height:22px!important;margin-left:3px;vertical-align:text-bottom;"
|
|
||||||
src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"
|
|
||||||
alt="by"
|
|
||||||
/><img
|
|
||||||
style="height:22px!important;margin-left:3px;vertical-align:text-bottom;"
|
|
||||||
src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"
|
|
||||||
alt="nc"
|
|
||||||
/><img
|
|
||||||
style="height:22px!important;margin-left:3px;vertical-align:text-bottom;"
|
|
||||||
src="https://mirrors.creativecommons.org/presskit/icons/nd.svg?ref=chooser-v1"
|
|
||||||
alt="nd"
|
|
||||||
/></a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
</footer>
|
|
@ -1,85 +0,0 @@
|
|||||||
import { globalStyle, style } from '@vanilla-extract/css'
|
|
||||||
import { radialGradient, transparentize } from 'polished'
|
|
||||||
import { menuBackground, transparent, vars } from '$lib/styles/vars.css'
|
|
||||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
|
||||||
|
|
||||||
export const navigationClass = style([
|
|
||||||
sprinkles({
|
|
||||||
paddingTop: '1x',
|
|
||||||
paddingBottom: '2x',
|
|
||||||
paddingX: '1x',
|
|
||||||
color: 'menu',
|
|
||||||
textShadow: 'menuLinkShadow',
|
|
||||||
}),
|
|
||||||
radialGradient({
|
|
||||||
colorStops: [
|
|
||||||
`${menuBackground} 0%`,
|
|
||||||
`${transparentize(1, menuBackground)} 100%`,
|
|
||||||
],
|
|
||||||
extent: '120% 100% at 0% 0%',
|
|
||||||
fallback: transparent,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
export const navigationContentClass = sprinkles({
|
|
||||||
display: 'flex',
|
|
||||||
maxWidth: 'max',
|
|
||||||
marginY: 'none',
|
|
||||||
marginX: 'auto',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const navigationLinksClass = sprinkles({
|
|
||||||
listStyle: 'none',
|
|
||||||
margin: 'none',
|
|
||||||
padding: 'none',
|
|
||||||
display: 'flex',
|
|
||||||
flex: '1',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const logoSectionClass = sprinkles({
|
|
||||||
lineHeight: 'none',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const logoLinkClass = sprinkles({
|
|
||||||
padding: 'none',
|
|
||||||
display: 'block',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${navigationClass} a:not(${logoLinkClass})`, {
|
|
||||||
color: vars.color.menuLink,
|
|
||||||
padding: vars.space['1x'],
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${navigationClass} a:hover`, {
|
|
||||||
color: vars.color.menuLinkHover,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const logoImgClass = style({
|
|
||||||
height: vars.space['3x'],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const selectedClass = sprinkles({
|
|
||||||
textShadow: 'menuActiveLinkShadow',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const portfolioPageNavigation = style({
|
|
||||||
position: 'sticky',
|
|
||||||
top: '0px',
|
|
||||||
zIndex: 1,
|
|
||||||
width: '100%',
|
|
||||||
fontSize: vars.fontSize.sm,
|
|
||||||
padding: vars.space['1x'],
|
|
||||||
background: vars.color.background,
|
|
||||||
boxShadow: `0px 0.5em 0.5em ${vars.color.background}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const portfolioPageNavigationLinksClass = sprinkles({
|
|
||||||
maxWidth: 'l',
|
|
||||||
marginX: 'auto',
|
|
||||||
marginY: 'none',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const portfolioPageNavigationLinkClass = sprinkles({
|
|
||||||
padding: '1x',
|
|
||||||
})
|
|
@ -1,94 +0,0 @@
|
|||||||
<script>
|
|
||||||
import classNames from 'classnames'
|
|
||||||
import {
|
|
||||||
logoImgClass,
|
|
||||||
logoLinkClass,
|
|
||||||
logoSectionClass,
|
|
||||||
navigationClass,
|
|
||||||
navigationContentClass,
|
|
||||||
navigationLinksClass,
|
|
||||||
portfolioPageNavigation,
|
|
||||||
portfolioPageNavigationLinkClass,
|
|
||||||
portfolioPageNavigationLinksClass,
|
|
||||||
selectedClass,
|
|
||||||
} from './Nav.css'
|
|
||||||
import { page } from '$app/stores'
|
|
||||||
|
|
||||||
$: segment = $page.url.pathname
|
|
||||||
|
|
||||||
let links = [
|
|
||||||
{
|
|
||||||
label: 'Introduction',
|
|
||||||
url: '/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Blog',
|
|
||||||
url: '/blog',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Broadcasts',
|
|
||||||
url: '/broadcasts',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// label: "Dev's Cookery",
|
|
||||||
// url: '/cookery',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: 'Portfolio',
|
|
||||||
url: '/portfolio',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<nav class={navigationClass}>
|
|
||||||
<section class={navigationContentClass}>
|
|
||||||
<ul class={navigationLinksClass}>
|
|
||||||
{#each links as link}
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
rel="prefetch"
|
|
||||||
class={classNames({ [selectedClass]: segment === link.url })}
|
|
||||||
href={link.url}
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<aside class="logo-section {logoSectionClass}">
|
|
||||||
<a class="logo {logoLinkClass}" href=".">
|
|
||||||
<img
|
|
||||||
class={logoImgClass}
|
|
||||||
src="/m-logo.svg"
|
|
||||||
alt="m logo"
|
|
||||||
width="44px"
|
|
||||||
height="44px"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</aside>
|
|
||||||
</section>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{#if segment === '/portfolio'}
|
|
||||||
<section class="page-navigation {portfolioPageNavigation}">
|
|
||||||
<div class={portfolioPageNavigationLinksClass}>
|
|
||||||
<a
|
|
||||||
class={portfolioPageNavigationLinkClass}
|
|
||||||
href="/portfolio#personal-information">About</a
|
|
||||||
>
|
|
||||||
<a class={portfolioPageNavigationLinkClass} href="/portfolio#skills"
|
|
||||||
>Skills</a
|
|
||||||
>
|
|
||||||
<a class={portfolioPageNavigationLinkClass} href="/portfolio#work-history"
|
|
||||||
>Work History</a
|
|
||||||
>
|
|
||||||
<a class={portfolioPageNavigationLinkClass} href="/portfolio#projects"
|
|
||||||
>Projects</a
|
|
||||||
>
|
|
||||||
<a class={portfolioPageNavigationLinkClass} href="/portfolio#education"
|
|
||||||
>Education</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
@ -1,9 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import svgSprite from '../../svg/build/icons-sprite.svg'
|
|
||||||
export let className: string
|
|
||||||
export let name: string
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg aria-hidden="true" class={className}>
|
|
||||||
<use xlink:href={`${svgSprite}#${name}`} />
|
|
||||||
</svg>
|
|
@ -1,18 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
interface ArticleDetails {
|
|
||||||
title: string
|
|
||||||
slug: string
|
|
||||||
preview: string
|
|
||||||
}
|
|
||||||
export let segment: string
|
|
||||||
export let article: ArticleDetails
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<h2>
|
|
||||||
<a rel="prefetch" href={`/${segment}/${article.slug}`}>{article.title}</a>
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
{@html article.preview}
|
|
||||||
</article>
|
|
@ -1,30 +0,0 @@
|
|||||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
|
||||||
|
|
||||||
export const tagsListClass = sprinkles({
|
|
||||||
listStyle: 'none',
|
|
||||||
margin: 'none',
|
|
||||||
padding: 'none',
|
|
||||||
display: 'inline',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const tagsListLiClass = sprinkles({
|
|
||||||
display: 'inline',
|
|
||||||
fontStyle: 'italic',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const publishedClass = sprinkles({
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
fontStyle: 'italic',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const publishedLabelClass = sprinkles({
|
|
||||||
color: 'tintedText',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const footerClass = sprinkles({
|
|
||||||
display: 'flex',
|
|
||||||
fontSize: 'sm',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
paddingTop: '1x',
|
|
||||||
marginTop: '2x',
|
|
||||||
})
|
|
@ -1,37 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { horizontalBorderTopClass } from '$lib/styles/scoops.css'
|
|
||||||
|
|
||||||
import { format } from 'date-fns'
|
|
||||||
import type { ArticleContent } from '$lib/content/articleContentListing'
|
|
||||||
import {
|
|
||||||
footerClass,
|
|
||||||
publishedClass,
|
|
||||||
publishedLabelClass,
|
|
||||||
tagsListClass,
|
|
||||||
tagsListLiClass,
|
|
||||||
} from './ArticlePreviewFooter.css'
|
|
||||||
|
|
||||||
export let segment: string
|
|
||||||
export let article: ArticleContent
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<footer class="{footerClass} {horizontalBorderTopClass}">
|
|
||||||
<div class="article-tags">
|
|
||||||
{#if article.tags.length > 0}
|
|
||||||
<span class="lighten">Tags:</span>
|
|
||||||
<ul class={tagsListClass}>
|
|
||||||
{#each article.tags as tag}
|
|
||||||
<li class={tagsListLiClass}>
|
|
||||||
<a href="/{segment}/tags/{tag}">{tag}</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="created-at">
|
|
||||||
<span class={publishedLabelClass}>Published on</span>
|
|
||||||
<time datetime={article.date} class={publishedClass}>
|
|
||||||
{format(new Date(article.date), "do MMMM',' y")}
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
@ -1,20 +0,0 @@
|
|||||||
import { globalStyle } from '@vanilla-extract/css'
|
|
||||||
import { vars } from '$lib/styles/vars.css'
|
|
||||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
|
||||||
|
|
||||||
export const postListClass = sprinkles({
|
|
||||||
padding: 'none',
|
|
||||||
lineHeight: '3x',
|
|
||||||
listStyle: 'none',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const seeAllClass = sprinkles({
|
|
||||||
textAlign: 'end',
|
|
||||||
width: 'parent',
|
|
||||||
maxWidth: 'max',
|
|
||||||
margin: 'auto',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${postListClass} > li:not(:last-child)`, {
|
|
||||||
marginBottom: vars.space['4x'],
|
|
||||||
})
|
|
@ -1,41 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ArticleFooter from '$lib/components/articles/ArticlePreviewFooter/ArticlePreviewFooter.svelte'
|
|
||||||
import Paginator from '$lib/components/paginator/Paginator.svelte'
|
|
||||||
import { postListClass } from './ArticlePreviewList.css'
|
|
||||||
import ArticlePreviewCard from '$lib/components/articles/ArticlePreviewCard/ArticlePreviewCard.svelte'
|
|
||||||
import type { PaginationResult } from '$lib/pagination/pagination'
|
|
||||||
import type { ArticleContent } from '$lib/content/articleContentListing'
|
|
||||||
|
|
||||||
export let page: number
|
|
||||||
export let pageSize: number
|
|
||||||
export let filters: Record<string, string>
|
|
||||||
export let posts: PaginationResult<ArticleContent>
|
|
||||||
export let segment: string
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<Paginator
|
|
||||||
{segment}
|
|
||||||
{page}
|
|
||||||
{pageSize}
|
|
||||||
{filters}
|
|
||||||
totalCount={posts.totalCount}
|
|
||||||
/>
|
|
||||||
</header>
|
|
||||||
<ul class="post-list {postListClass}">
|
|
||||||
{#each posts.items as article (article.slug)}
|
|
||||||
<li>
|
|
||||||
<ArticlePreviewCard {article} {segment} />
|
|
||||||
<ArticleFooter {article} {segment} />
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
<footer>
|
|
||||||
<Paginator
|
|
||||||
{segment}
|
|
||||||
{page}
|
|
||||||
{pageSize}
|
|
||||||
{filters}
|
|
||||||
totalCount={posts.totalCount}
|
|
||||||
/>
|
|
||||||
</footer>
|
|
@ -1,21 +0,0 @@
|
|||||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
|
||||||
|
|
||||||
export const listClass = sprinkles({
|
|
||||||
listStyle: 'none',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const listItemClass = sprinkles({
|
|
||||||
paddingX: '1x',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const activePage = sprinkles({
|
|
||||||
//fontStyle: 'italic',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
paddingX: '2x',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const pageLinkClass = sprinkles({
|
|
||||||
paddingX: '1x',
|
|
||||||
})
|
|
@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
activePage,
|
|
||||||
listClass,
|
|
||||||
listItemClass,
|
|
||||||
pageLinkClass,
|
|
||||||
} from './Paginator.css'
|
|
||||||
|
|
||||||
import { getPaginatorPages, createHref } from './paginatorUtils'
|
|
||||||
|
|
||||||
export const Divider = 'divider'
|
|
||||||
|
|
||||||
export let segment: string
|
|
||||||
export let page: number
|
|
||||||
export let pageSize: number
|
|
||||||
export let totalCount: number
|
|
||||||
export let filters: Record<string, string>
|
|
||||||
|
|
||||||
$: paginatorPages = getPaginatorPages({ page, pageSize, totalCount })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ul class={listClass}>
|
|
||||||
{#if page !== 1}
|
|
||||||
<li class="{listItemClass} ">
|
|
||||||
<a class={pageLinkClass} href={createHref(segment, filters, page - 1)}
|
|
||||||
><</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{#each paginatorPages as pageNumber}
|
|
||||||
{#if pageNumber === Divider}
|
|
||||||
<li class={listItemClass}>...</li>
|
|
||||||
{:else if page === pageNumber}
|
|
||||||
<li class="{listItemClass} {activePage}">{pageNumber}</li>
|
|
||||||
{:else}
|
|
||||||
<li class="{listItemClass} ">
|
|
||||||
<a class={pageLinkClass} href={createHref(segment, filters, pageNumber)}
|
|
||||||
>{pageNumber}</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{#if page !== paginatorPages.length}
|
|
||||||
<li class="{listItemClass} ">
|
|
||||||
<a class={pageLinkClass} href={createHref(segment, filters, page + 1)}
|
|
||||||
>></a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
@ -1,52 +0,0 @@
|
|||||||
import { describe, expect, test } from 'vitest'
|
|
||||||
import { Divider, getPaginatorPages } from './paginatorUtils'
|
|
||||||
|
|
||||||
describe('Paginator component', () => {
|
|
||||||
describe('Paginator generates feasable pages to display', () => {
|
|
||||||
test('Page: 1/5', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 1, totalCount: 5, pageSize: 1 })
|
|
||||||
).toEqual([1, 2, 3, 4, 5])
|
|
||||||
})
|
|
||||||
test('Page: 4/7', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 4, totalCount: 7, pageSize: 1 })
|
|
||||||
).toEqual([1, 2, 3, 4, 5, 6, 7])
|
|
||||||
})
|
|
||||||
test('Page: 4/8', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 4, totalCount: 8, pageSize: 1 })
|
|
||||||
).toEqual([1, 2, 3, 4, 5, 6, Divider, 8])
|
|
||||||
})
|
|
||||||
test('Page: 1/10', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 1, totalCount: 10, pageSize: 1 })
|
|
||||||
).toEqual([1, 2, 3, 4, 5, 6, Divider, 10])
|
|
||||||
})
|
|
||||||
test('Page: 2/10', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 2, totalCount: 10, pageSize: 1 })
|
|
||||||
).toEqual([1, 2, 3, 4, 5, 6, Divider, 10])
|
|
||||||
})
|
|
||||||
test('Page: 5/10', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 5, totalCount: 10, pageSize: 1 })
|
|
||||||
).toEqual([1, Divider, 3, 4, 5, 6, 7, Divider, 10])
|
|
||||||
})
|
|
||||||
test('Page: 7/10', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 7, totalCount: 10, pageSize: 1 })
|
|
||||||
).toEqual([1, Divider, 5, 6, 7, 8, 9, 10])
|
|
||||||
})
|
|
||||||
test('Page: 8/10', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 8, totalCount: 10, pageSize: 1 })
|
|
||||||
).toEqual([1, Divider, 5, 6, 7, 8, 9, 10])
|
|
||||||
})
|
|
||||||
test('Page: 10/10', () => {
|
|
||||||
expect(
|
|
||||||
getPaginatorPages({ page: 10, totalCount: 10, pageSize: 1 })
|
|
||||||
).toEqual([1, Divider, 5, 6, 7, 8, 9, 10])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,51 +0,0 @@
|
|||||||
import { toParams } from '$lib/pagination/dropTakeParams'
|
|
||||||
import { last, range } from 'ramda'
|
|
||||||
|
|
||||||
export const Divider = 'divider'
|
|
||||||
|
|
||||||
export function getPaginatorPages({
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
totalCount,
|
|
||||||
}: {
|
|
||||||
page: number
|
|
||||||
pageSize: number
|
|
||||||
totalCount: number
|
|
||||||
}): (number | typeof Divider)[] {
|
|
||||||
const maxLinksLength = 7
|
|
||||||
const linksAroundActive = 2
|
|
||||||
const totalPages = Math.ceil(totalCount / pageSize)
|
|
||||||
const shownPages = range(1, totalPages + 1).reduce<
|
|
||||||
(number | typeof Divider)[]
|
|
||||||
>((acc, link) => {
|
|
||||||
const isFirst = link === 1
|
|
||||||
const isLast = link === totalPages
|
|
||||||
const isPageOnStart = page <= 3 && link < maxLinksLength
|
|
||||||
const isPageOnEnd =
|
|
||||||
page >= totalPages - 3 && link > totalPages - maxLinksLength + 1
|
|
||||||
|
|
||||||
if ([isFirst, isLast, isPageOnStart, isPageOnEnd].some((value) => value)) {
|
|
||||||
return [...acc, link]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link < page - linksAroundActive || link > page + linksAroundActive) {
|
|
||||||
if (last(acc) === Divider) {
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
return [...acc, Divider]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...acc, link]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return shownPages
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createHref(
|
|
||||||
href: string,
|
|
||||||
filters: Record<string, string>,
|
|
||||||
pageNumber: number
|
|
||||||
) {
|
|
||||||
const filtersPath = toParams(filters)
|
|
||||||
return `/${href}/${filtersPath ? filtersPath + '/' : ''}page/${pageNumber}`
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
import { map, multiply } from 'ramda'
|
|
||||||
|
|
||||||
export interface ImageOptions {
|
|
||||||
width?: number
|
|
||||||
height?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URL for resource with specified parameters for Netlify Large Media trasformation
|
|
||||||
*
|
|
||||||
* @see https://docs.netlify.com/large-media/transform-images/
|
|
||||||
*/
|
|
||||||
export function getNFResize(href: string, { width, height }: ImageOptions) {
|
|
||||||
return `${href}?nf_resize=fit${height ? `&h=${height}` : ''}${
|
|
||||||
width ? `&w=${width}` : ''
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PIXEL_DENSITIES = [1, 1.5, 2, 3, 4]
|
|
||||||
|
|
||||||
function multiplyImageOptions(
|
|
||||||
multiplier,
|
|
||||||
imageOptions: ImageOptions
|
|
||||||
): ImageOptions {
|
|
||||||
return map(multiply(multiplier), imageOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate `srcset` attribute for all `PIXEL_DENSITIES` to serve images in appropriate quality
|
|
||||||
* for each device with specific density
|
|
||||||
*
|
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset
|
|
||||||
*/
|
|
||||||
export function generateSrcSet(href: string, imageOptions: ImageOptions) {
|
|
||||||
return PIXEL_DENSITIES.map(
|
|
||||||
(density) =>
|
|
||||||
`${getNFResize(
|
|
||||||
href,
|
|
||||||
multiplyImageOptions(density, imageOptions)
|
|
||||||
)} ${density}x`
|
|
||||||
).join(',')
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import marked from 'marked'
|
|
||||||
import { renderer } from './renderer-extension'
|
|
||||||
|
|
||||||
marked.use({ renderer })
|
|
||||||
|
|
||||||
export function parseField<T>(field: keyof T) {
|
|
||||||
return (item: T): T => ({
|
|
||||||
...item,
|
|
||||||
[field]: marked(item[field]),
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
import { generateSrcSet, getNFResize } from '$lib/large-media'
|
|
||||||
import Prism from 'prismjs'
|
|
||||||
import loadLanguages from 'prismjs/components/index.js'
|
|
||||||
|
|
||||||
loadLanguages(['bash', 'markdown', 'json', 'yaml', 'typescript'])
|
|
||||||
|
|
||||||
export const renderer = {
|
|
||||||
heading(text: string, level: string) {
|
|
||||||
const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-')
|
|
||||||
|
|
||||||
return `
|
|
||||||
<h${level}>
|
|
||||||
<a name="${escapedText}" class="anchor" href="#${escapedText}">
|
|
||||||
<span class="header-link"></span>
|
|
||||||
</a>
|
|
||||||
${text}
|
|
||||||
</h${level}>
|
|
||||||
`
|
|
||||||
},
|
|
||||||
image(href: string, title: string, text: string) {
|
|
||||||
const figcaption = title ? `<figcaption>${title}</figcaption>` : ''
|
|
||||||
const isLocal = !href.startsWith('http')
|
|
||||||
const src = isLocal ? getNFResize(href, { height: 800, width: 800 }) : href
|
|
||||||
const srcset = isLocal
|
|
||||||
? `srcset="${generateSrcSet(href, { width: 800, height: 800 })}"`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return `
|
|
||||||
<figure>
|
|
||||||
<img
|
|
||||||
alt="${text}"
|
|
||||||
${srcset}
|
|
||||||
src="${src}"
|
|
||||||
/>
|
|
||||||
${figcaption}
|
|
||||||
</figure>
|
|
||||||
`
|
|
||||||
},
|
|
||||||
code(source: string, lang?: string) {
|
|
||||||
// When lang is not specified it is usually an empty string which has to be handled
|
|
||||||
const usedLang = !lang ? 'shell' : lang
|
|
||||||
const highlightedSource = Prism.highlight(
|
|
||||||
source,
|
|
||||||
Prism.languages[usedLang],
|
|
||||||
usedLang
|
|
||||||
)
|
|
||||||
return `<pre class='language-${usedLang}'><code class='language-${usedLang}'>${highlightedSource}</code></pre>`
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import { describe, test, expect } from 'vitest'
|
|
||||||
import { getDropTakeFromPageParams } from './dropTakeParams'
|
|
||||||
|
|
||||||
describe('convert search params', () => {
|
|
||||||
test('should convert from page size and page to offset and limit', () => {
|
|
||||||
expect(getDropTakeFromPageParams(7, 2)).toEqual({
|
|
||||||
offset: 7,
|
|
||||||
limit: 7,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,42 +0,0 @@
|
|||||||
import { init, splitEvery } from 'ramda'
|
|
||||||
|
|
||||||
export function parseParams(params: string) {
|
|
||||||
let splittedParams = params.split('/')
|
|
||||||
if (splittedParams.length % 2 !== 0) {
|
|
||||||
splittedParams = init(splittedParams)
|
|
||||||
}
|
|
||||||
const splits = splitEvery(2, splittedParams)
|
|
||||||
return Object.fromEntries(splits)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toParams(records: Record<string, string>) {
|
|
||||||
return Object.entries(records)
|
|
||||||
.map(([key, value]) => `${key}/${value}`)
|
|
||||||
.join('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginationParams {
|
|
||||||
pageSize: number
|
|
||||||
page: number
|
|
||||||
filters?: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DropTakeParams {
|
|
||||||
offset: number
|
|
||||||
limit: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert svelte `load` params into a `offset` and `limit` so they can be used to fetch endpoints with pagination queries
|
|
||||||
*/
|
|
||||||
export function getDropTakeFromPageParams(
|
|
||||||
pageSize: number,
|
|
||||||
page: number
|
|
||||||
): DropTakeParams {
|
|
||||||
const offset = pageSize * (page - 1)
|
|
||||||
const limit = pageSize
|
|
||||||
return {
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
import { range } from 'ramda'
|
|
||||||
import { describe, expect, test } from 'vitest'
|
|
||||||
import { filterByPropContains, dropAndTake, filterAndCount } from './pagination'
|
|
||||||
|
|
||||||
describe('pagination', () => {
|
|
||||||
test('does not drop any items by default', () => {
|
|
||||||
const items = range(0, 100)
|
|
||||||
expect(dropAndTake({})(items)).toHaveLength(100)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('limits out exact number of items', () => {
|
|
||||||
const items = range(0, 100)
|
|
||||||
expect(dropAndTake({ limit: 10 })(items)).toHaveLength(10)
|
|
||||||
expect(dropAndTake({ limit: 10 })(items)[0]).toBe(0)
|
|
||||||
expect(dropAndTake({ limit: 10 })(items)[9]).toBe(9)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('offset is skipping a number of items from the front', () => {
|
|
||||||
const items = range(0, 100)
|
|
||||||
expect(dropAndTake({ offset: 10 })(items)).toHaveLength(90)
|
|
||||||
expect(dropAndTake({ offset: 10 })(items)[0]).toBe(10)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('is able to combine limit and offset', () => {
|
|
||||||
const items = range(0, 100)
|
|
||||||
expect(dropAndTake({ offset: 10, limit: 10 })(items)).toHaveLength(10)
|
|
||||||
expect(dropAndTake({ offset: 10, limit: 10 })(items)[0]).toBe(10)
|
|
||||||
expect(dropAndTake({ offset: 10, limit: 10 })(items)[9]).toBe(19)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('is able to filter by a field', () => {
|
|
||||||
const items = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
prop: ['yes'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
prop: ['yes', 'no'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
expect(filterByPropContains({ prop: 'no' })(items)).toHaveLength(1)
|
|
||||||
expect(filterByPropContains({ prop: 'no' })(items)[0].id).toBe(2)
|
|
||||||
|
|
||||||
expect(filterByPropContains({ prop: 'yes' })(items)[0].id).toBe(1)
|
|
||||||
expect(filterByPropContains({ prop: 'yes' })(items)).toHaveLength(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('is able to combine limit and offset while filtering by field', () => {
|
|
||||||
const items = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
prop: ['yes'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
prop: ['yes', 'no'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
prop: ['yes', 'no'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
test('combine all parameters', () => {
|
|
||||||
const result = filterAndCount({
|
|
||||||
offset: 1,
|
|
||||||
limit: 1,
|
|
||||||
filters: { prop: 'no' },
|
|
||||||
})(items)
|
|
||||||
expect(result.totalCount).toBe(2)
|
|
||||||
expect(result.items[0].id).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('with 0 offset', () => {
|
|
||||||
const result = filterAndCount({
|
|
||||||
offset: 0,
|
|
||||||
limit: 1,
|
|
||||||
filters: { prop: 'no' },
|
|
||||||
})(items)
|
|
||||||
expect(result.totalCount).toBe(2)
|
|
||||||
expect(result.items[0].id).toBe(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('without filter', () => {
|
|
||||||
const result = filterAndCount({ offset: 1, limit: 1 })(items)
|
|
||||||
expect(result.totalCount).toBe(3)
|
|
||||||
expect(result.items[0].id).toBe(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('without any params', () => {
|
|
||||||
const result = filterAndCount({})(items)
|
|
||||||
expect(result.totalCount).toBe(3)
|
|
||||||
expect(result.items.length).toEqual(result.totalCount)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,48 +0,0 @@
|
|||||||
import { identity, drop, take, pipe } from 'ramda'
|
|
||||||
|
|
||||||
export interface PaginationQuery {
|
|
||||||
offset?: number
|
|
||||||
limit?: number
|
|
||||||
filters?: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginationResult<ItemType> {
|
|
||||||
items: ItemType[]
|
|
||||||
totalCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dropAndTake<Item>({ offset = 0, limit = Infinity }) {
|
|
||||||
return pipe(drop<Item>(offset), take<Item>(limit)) as (
|
|
||||||
items: Item[]
|
|
||||||
) => Item[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function filterByPropContains<Item extends Record<string, any>>(
|
|
||||||
filters: Record<string, string>
|
|
||||||
) {
|
|
||||||
return function (items: Item[]) {
|
|
||||||
return items.filter((item) => {
|
|
||||||
return Object.entries(filters).every(([fieldName, value]) =>
|
|
||||||
item[fieldName].includes(value)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function filterAndCount<Item extends Record<string, any>>({
|
|
||||||
filters,
|
|
||||||
...dropTakeParams
|
|
||||||
}: PaginationQuery) {
|
|
||||||
return function (items: Item[]) {
|
|
||||||
const filterFunction = filters
|
|
||||||
? filterByPropContains<Item>(filters)
|
|
||||||
: identity
|
|
||||||
const filteredItems = filterFunction(items)
|
|
||||||
return {
|
|
||||||
items: dropAndTake<Item>(dropTakeParams)(filteredItems),
|
|
||||||
totalCount: filteredItems.length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import { globalStyle, style } from '@vanilla-extract/css'
|
|
||||||
import { vars } from '$lib/styles/vars.css'
|
|
||||||
|
|
||||||
export const contentClass = style({})
|
|
||||||
|
|
||||||
globalStyle(`${contentClass} ul, ${contentClass} ol`, {
|
|
||||||
lineHeight: vars.lineHeight['2x'],
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${contentClass} li`, {
|
|
||||||
marginBottom: vars.space['2x'],
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${contentClass} img`, {
|
|
||||||
maxHeight: vars.height.image,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${contentClass} img:only-child`, {
|
|
||||||
display: 'block',
|
|
||||||
margin: '0 auto',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle(`${contentClass} .video-embed`, {
|
|
||||||
margin: '0 auto',
|
|
||||||
maxWidth: vars.width.image,
|
|
||||||
aspectRatio: vars.aspectRatio.monitor,
|
|
||||||
})
|
|
@ -1,155 +0,0 @@
|
|||||||
import { globalStyle } from '@vanilla-extract/css'
|
|
||||||
import { breakpoints, colors, vars } from './vars.css'
|
|
||||||
|
|
||||||
globalStyle('html', {
|
|
||||||
scrollBehavior: 'smooth',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('body', {
|
|
||||||
margin: 0,
|
|
||||||
fontFamily:
|
|
||||||
'cantarell, roboto, -apple-system, blinkmacsystemfont, segoe ui, oxygen, ubuntu, fira sans, droid sans, helvetica neue, sans-serif',
|
|
||||||
fontSize: '16px',
|
|
||||||
lineHeight: 1.65,
|
|
||||||
color: vars.color.articleText,
|
|
||||||
background: vars.color.background,
|
|
||||||
minHeight: '100vh',
|
|
||||||
'@media': {
|
|
||||||
[`screen and (min-width: ${breakpoints.s}px)`]: {
|
|
||||||
fontSize: '18px',
|
|
||||||
},
|
|
||||||
[`screen and (min-width: ${breakpoints.m}px)`]: {
|
|
||||||
fontSize: '24px',
|
|
||||||
},
|
|
||||||
print: {
|
|
||||||
fontSize: '12px',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('h1, h2, h3, h4, h5, h6', {
|
|
||||||
marginTop: vars.space['2x'],
|
|
||||||
marginBottom: vars.space['1x'],
|
|
||||||
marginLeft: vars.space.none,
|
|
||||||
marginRight: vars.space.none,
|
|
||||||
lineHeight: vars.lineHeight['1x'],
|
|
||||||
color: vars.color.header,
|
|
||||||
fontWeight: 500,
|
|
||||||
letterSpacing: '-0.01em',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('h1', {
|
|
||||||
fontSize: vars.fontSize['5x'],
|
|
||||||
fontWeight: 800,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('h2', {
|
|
||||||
fontSize: vars.fontSize['4x'],
|
|
||||||
fontWeight: 700,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('h3', {
|
|
||||||
fontSize: vars.fontSize['3x'],
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('h4', {
|
|
||||||
fontSize: vars.space['2x'],
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('a', {
|
|
||||||
textDecoration: 'none',
|
|
||||||
transition: 'color 0.2s',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('a:link', {
|
|
||||||
color: vars.color.link,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('a:hover', {
|
|
||||||
color: vars.color.linkHover,
|
|
||||||
textDecoration: 'underline',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('a:visited', {
|
|
||||||
color: vars.color.linkVisited,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('a:visited:hover', {
|
|
||||||
color: vars.color.linkVisitedHover,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('main pre, main pre[class*="language-"], main :not(pre) > code', {
|
|
||||||
fontFamily: 'menlo, inconsolata, monospace',
|
|
||||||
backgroundColor: vars.color.codeBackground,
|
|
||||||
paddingTop: vars.space['1x'],
|
|
||||||
paddingBottom: vars.space['1x'],
|
|
||||||
paddingLeft: vars.space['1x'],
|
|
||||||
paddingRight: vars.space['1x'],
|
|
||||||
color: vars.color.code,
|
|
||||||
lineHeight: vars.lineHeight['0x'],
|
|
||||||
boxShadow: vars.boxShadow.codeBoxShadow,
|
|
||||||
borderRadius: 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('main code, main code[class*="language-"]', {
|
|
||||||
fontSize: vars.fontSize.sm,
|
|
||||||
'@media': {
|
|
||||||
[`screen and (min-width: ${breakpoints.m}px)`]: {
|
|
||||||
fontSize: vars.fontSize.xs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('code', {
|
|
||||||
whiteSpace: 'pre-line',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('pre code', {
|
|
||||||
whiteSpace: 'pre',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('figure', {
|
|
||||||
marginTop: vars.space['2x'],
|
|
||||||
marginBottom: vars.space['2x'],
|
|
||||||
marginLeft: vars.space['1x'],
|
|
||||||
marginRight: vars.space['1x'],
|
|
||||||
textAlign: 'center',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('figcaption', {
|
|
||||||
fontSize: vars.fontSize.xs,
|
|
||||||
fontStyle: 'italic',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('blockquote', {
|
|
||||||
lineHeight: vars.lineHeight['2x'],
|
|
||||||
margin: vars.space['0x'],
|
|
||||||
paddingLeft: vars.space['2x'],
|
|
||||||
paddingRight: vars.space['0x'],
|
|
||||||
paddingTop: vars.space['1x'],
|
|
||||||
paddingBottom: vars.space['2x'],
|
|
||||||
background: vars.color.quoteBackground,
|
|
||||||
borderRadius: 3,
|
|
||||||
borderLeft: `2px solid ${colors.tearkiss}`,
|
|
||||||
boxShadow: vars.boxShadow.contentBoxShadow,
|
|
||||||
fontSize: vars.fontSize.sm,
|
|
||||||
overflow: 'auto',
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('blockquote p', {
|
|
||||||
marginTop: vars.space['0x'],
|
|
||||||
marginBottom: vars.space['0x'],
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('p', {
|
|
||||||
marginTop: vars.space['1x'],
|
|
||||||
marginBottom: vars.space['1x'],
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('b, strong', {
|
|
||||||
fontWeight: 600,
|
|
||||||
})
|
|
||||||
|
|
||||||
globalStyle('::selection', {
|
|
||||||
background: vars.color.selection,
|
|
||||||
})
|
|
@ -1,10 +0,0 @@
|
|||||||
import { style } from '@vanilla-extract/css'
|
|
||||||
import { desaturate, transparentize } from 'polished'
|
|
||||||
import { colors } from './vars.css'
|
|
||||||
|
|
||||||
export const horizontalBorderTopClass = style({
|
|
||||||
borderTop: `1px solid ${transparentize(
|
|
||||||
0.6,
|
|
||||||
desaturate(0.5, colors.tearkiss)
|
|
||||||
)}`,
|
|
||||||
})
|
|
@ -1,80 +0,0 @@
|
|||||||
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles'
|
|
||||||
import { breakpoints, vars } from './vars.css'
|
|
||||||
|
|
||||||
const responsiveProperties = defineProperties({
|
|
||||||
conditions: {
|
|
||||||
mobile: {},
|
|
||||||
tablet: { '@media': `screen and (min-width: ${breakpoints.m}px)` },
|
|
||||||
desktop: { '@media': `screen and (min-width: ${breakpoints.l}px)` },
|
|
||||||
},
|
|
||||||
defaultCondition: 'mobile',
|
|
||||||
properties: {
|
|
||||||
display: ['none', 'flex', 'block', 'inline', 'inline-block', 'grid'],
|
|
||||||
position: ['relative', 'absolute', 'fixed'],
|
|
||||||
flexDirection: ['row', 'column'],
|
|
||||||
flexWrap: ['wrap', 'nowrap'],
|
|
||||||
flexShrink: [0],
|
|
||||||
flexGrow: [0, 1],
|
|
||||||
justifyContent: [
|
|
||||||
'stretch',
|
|
||||||
'start',
|
|
||||||
'center',
|
|
||||||
'end',
|
|
||||||
'space-around',
|
|
||||||
'space-between',
|
|
||||||
],
|
|
||||||
justifyItems: [
|
|
||||||
'stretch',
|
|
||||||
'start',
|
|
||||||
'center',
|
|
||||||
'end',
|
|
||||||
'space-around',
|
|
||||||
'space-between',
|
|
||||||
],
|
|
||||||
alignItems: ['stretch', 'flex-start', 'center', 'flex-end'],
|
|
||||||
flex: ['1'],
|
|
||||||
gap: vars.space,
|
|
||||||
textAlign: ['center', 'justify', 'start', 'end'],
|
|
||||||
textShadow: vars.textShadow,
|
|
||||||
paddingTop: vars.space,
|
|
||||||
paddingBottom: vars.space,
|
|
||||||
paddingLeft: vars.space,
|
|
||||||
paddingRight: vars.space,
|
|
||||||
marginTop: vars.space,
|
|
||||||
marginBottom: vars.space,
|
|
||||||
marginRight: vars.space,
|
|
||||||
marginLeft: vars.space,
|
|
||||||
columnGap: vars.space,
|
|
||||||
fontSize: vars.fontSize,
|
|
||||||
fontFamily: vars.fontFamily,
|
|
||||||
fontWeight: vars.fontWeight,
|
|
||||||
fontStyle: ['italic', 'normal'],
|
|
||||||
lineHeight: vars.lineHeight,
|
|
||||||
whiteSpace: ['normal', 'nowrap'],
|
|
||||||
width: vars.width,
|
|
||||||
maxWidth: vars.width,
|
|
||||||
height: vars.height,
|
|
||||||
listStyle: ['none'],
|
|
||||||
overflow: ['auto'],
|
|
||||||
aspectRatio: vars.aspectRatio,
|
|
||||||
},
|
|
||||||
shorthands: {
|
|
||||||
padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
|
|
||||||
paddingX: ['paddingLeft', 'paddingRight'],
|
|
||||||
paddingY: ['paddingTop', 'paddingBottom'],
|
|
||||||
placeItems: ['justifyContent', 'alignItems'],
|
|
||||||
typeSize: ['fontSize', 'lineHeight'],
|
|
||||||
margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
|
|
||||||
marginX: ['marginLeft', 'marginRight'],
|
|
||||||
marginY: ['marginTop', 'marginBottom'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const colorProperties = defineProperties({
|
|
||||||
properties: {
|
|
||||||
color: vars.color,
|
|
||||||
background: vars.color,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const sprinkles = createSprinkles(responsiveProperties, colorProperties)
|
|
@ -1,154 +0,0 @@
|
|||||||
import { createGlobalTheme } from '@vanilla-extract/css'
|
|
||||||
import {
|
|
||||||
darken,
|
|
||||||
desaturate,
|
|
||||||
lighten,
|
|
||||||
mix,
|
|
||||||
modularScale,
|
|
||||||
saturate,
|
|
||||||
tint,
|
|
||||||
transparentize,
|
|
||||||
} from 'polished'
|
|
||||||
|
|
||||||
export const colors = {
|
|
||||||
tearkiss: '#42a6f0',
|
|
||||||
pinky: '#fea6eb',
|
|
||||||
lightCyan: '#d8f6ff',
|
|
||||||
midnightBlue: '#171664',
|
|
||||||
frenchViolet: '#7332c3',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const menuBackground = transparentize(0.6, colors.tearkiss)
|
|
||||||
export const twitchEmbedBackground = transparentize(0.6, colors.pinky)
|
|
||||||
export const background = tint(0.7, colors.lightCyan)
|
|
||||||
export const codeBackground = tint(0.2, background)
|
|
||||||
export const quoteBackground = darken(0.02, background)
|
|
||||||
export const transparent = transparentize(1, '#ffffff')
|
|
||||||
const articleText = desaturate(0.16, colors.midnightBlue)
|
|
||||||
|
|
||||||
export enum breakpoints {
|
|
||||||
s = 400,
|
|
||||||
m = 700,
|
|
||||||
image = 800,
|
|
||||||
l = 1000,
|
|
||||||
max = 1140,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mediaAt(breakpoint: breakpoints) {
|
|
||||||
return `screen and (min-width: ${breakpoint}px)`
|
|
||||||
}
|
|
||||||
|
|
||||||
const createScale =
|
|
||||||
(base: number, ratio: number, unit = 'em') =>
|
|
||||||
(steps: number) =>
|
|
||||||
`${modularScale(steps, base, ratio)}${unit}`
|
|
||||||
|
|
||||||
const spaceScale = createScale(0.2, 2)
|
|
||||||
const fontSizeScale = createScale(1, 1.125)
|
|
||||||
const lineHeightScale = createScale(1.05, 1.125)
|
|
||||||
// const borderRadiusScale = createScale(1.5, 4)
|
|
||||||
|
|
||||||
export const vars = createGlobalTheme(':root', {
|
|
||||||
space: {
|
|
||||||
none: '0',
|
|
||||||
auto: 'auto',
|
|
||||||
'0x': spaceScale(0),
|
|
||||||
'1x': spaceScale(1),
|
|
||||||
'2x': spaceScale(2),
|
|
||||||
'3x': spaceScale(3),
|
|
||||||
'4x': spaceScale(4),
|
|
||||||
'5x': spaceScale(5),
|
|
||||||
'6x': spaceScale(6),
|
|
||||||
'7x': spaceScale(7),
|
|
||||||
'8x': spaceScale(8),
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
articleText,
|
|
||||||
tintedText: tint(0.25, articleText),
|
|
||||||
selection: tint(0.4, colors.pinky),
|
|
||||||
link: saturate(0.2, mix(0.66, colors.tearkiss, colors.midnightBlue)),
|
|
||||||
linkHover: colors.tearkiss,
|
|
||||||
linkVisited: colors.frenchViolet,
|
|
||||||
linkVisitedHover: lighten(0.1, colors.frenchViolet),
|
|
||||||
code: lighten(0.15, articleText),
|
|
||||||
|
|
||||||
menu: colors.midnightBlue,
|
|
||||||
menuLink: colors.midnightBlue,
|
|
||||||
menuLinkHover: lighten(0.15, colors.midnightBlue),
|
|
||||||
|
|
||||||
header: lighten(0.1, colors.midnightBlue),
|
|
||||||
background,
|
|
||||||
codeBackground,
|
|
||||||
quoteBackground,
|
|
||||||
menuBackground,
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
|
||||||
},
|
|
||||||
fontSize: {
|
|
||||||
xs: fontSizeScale(-2),
|
|
||||||
sm: fontSizeScale(-1),
|
|
||||||
base: fontSizeScale(0),
|
|
||||||
xl: fontSizeScale(1),
|
|
||||||
'2x': fontSizeScale(2),
|
|
||||||
'3x': fontSizeScale(3),
|
|
||||||
'4x': fontSizeScale(5),
|
|
||||||
'5x': fontSizeScale(7),
|
|
||||||
'6x': fontSizeScale(9),
|
|
||||||
},
|
|
||||||
lineHeight: {
|
|
||||||
none: '0',
|
|
||||||
'0x': lineHeightScale(0),
|
|
||||||
'1x': lineHeightScale(1),
|
|
||||||
'2x': lineHeightScale(2),
|
|
||||||
'3x': lineHeightScale(3),
|
|
||||||
'4x': lineHeightScale(4),
|
|
||||||
'5x': lineHeightScale(5),
|
|
||||||
},
|
|
||||||
fontWeight: {
|
|
||||||
thin: 'thin',
|
|
||||||
normal: 'normal',
|
|
||||||
bold: 'bold',
|
|
||||||
},
|
|
||||||
textShadow: {
|
|
||||||
menuLinkShadow: `0.02em 0.02em 0.03em ${transparentize(
|
|
||||||
0.7,
|
|
||||||
colors.midnightBlue
|
|
||||||
)}`,
|
|
||||||
menuActiveLinkShadow: `0.01em 0.01em 0.05em ${transparentize(
|
|
||||||
0.1,
|
|
||||||
colors.midnightBlue
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
contentBoxShadow: `0px 0px 2px 1px ${transparentize(
|
|
||||||
0.5,
|
|
||||||
desaturate(0.5, colors.tearkiss)
|
|
||||||
)}`,
|
|
||||||
codeBoxShadow: `inset 0px 0px 2px 1px ${transparentize(
|
|
||||||
0.8,
|
|
||||||
desaturate(0.5, colors.tearkiss)
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
auto: 'auto',
|
|
||||||
s: '400px',
|
|
||||||
m: '700px',
|
|
||||||
image: '800px',
|
|
||||||
l: '1000px',
|
|
||||||
max: '1140px',
|
|
||||||
full: '100vw',
|
|
||||||
parent: '100%',
|
|
||||||
layoutMax: '42rem',
|
|
||||||
headerFooterMax: '52rem',
|
|
||||||
additionalBlockMax: '46rem',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
full: '100hw',
|
|
||||||
parent: '100%',
|
|
||||||
image: '640px',
|
|
||||||
},
|
|
||||||
aspectRatio: {
|
|
||||||
monitor: '16 / 9',
|
|
||||||
},
|
|
||||||
})
|
|
@ -31,15 +31,14 @@ async fn main() {
|
|||||||
// build our application with a single route
|
// build our application with a single route
|
||||||
let app = router::get_router()
|
let app = router::get_router()
|
||||||
.nest_service("/styles", ServeDir::new("styles"))
|
.nest_service("/styles", ServeDir::new("styles"))
|
||||||
.nest_service("/images", ServeDir::new("../static/images"))
|
.nest_service("/images", ServeDir::new("static/images"))
|
||||||
.nest_service("/fonts", ServeDir::new("../static/fonts"))
|
.nest_service("/fonts", ServeDir::new("static/fonts"))
|
||||||
|
.nest_service("/files", ServeDir::new("static/files"))
|
||||||
.nest_service("/generated_images", ServeDir::new("generated_images"))
|
.nest_service("/generated_images", ServeDir::new("generated_images"))
|
||||||
.nest_service("/svg", ServeDir::new("../static/svg"))
|
.nest_service("/egg-fetcher", ServeDir::new("static/egg-fetcher"))
|
||||||
// TODO manifest logos have bad link, #directory-swap
|
.nest_service("/svg", ServeDir::new("static/svg"))
|
||||||
.nest_service(
|
.nest_service("/config.yml", ServeDir::new("static/resources/config.yml")) // Decap CMS config
|
||||||
"/config.yml",
|
.nest_service("/robots.txt", ServeDir::new("robots.txt"));
|
||||||
ServeDir::new("../static/resources/config.yml"),
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
let app = app.layer(LiveReloadLayer::new());
|
let app = app.layer(LiveReloadLayer::new());
|
||||||
@ -54,11 +53,8 @@ async fn main() {
|
|||||||
|
|
||||||
// TODO Socials
|
// TODO Socials
|
||||||
// - fotos
|
// - fotos
|
||||||
// background gradient color
|
|
||||||
// TODO Change DNS system
|
|
||||||
// THINK deploy to alula? rather then katelyn? can be change whenever
|
// THINK deploy to alula? rather then katelyn? can be change whenever
|
||||||
// TODO after release
|
//
|
||||||
// OG tags
|
// TODO 404 page
|
||||||
// Remove old web completely
|
// TODO view page transitions
|
||||||
// Restructure repository
|
// TODO cookbook
|
||||||
// - projects page
|
|
63
src/pages/blog_post_list.rs
Normal file
63
src/pages/blog_post_list.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{OriginalUri, Path},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use tokio::try_join;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
blog_posts::blog_post_model::{BlogPostMetadata, Segment, BLOG_POST_PATH},
|
||||||
|
components::site_header::{HeaderProps, Link},
|
||||||
|
post_utils::{
|
||||||
|
post_listing::get_post_list,
|
||||||
|
segments::get_posts_by_segment,
|
||||||
|
tags::{get_popular_tags, get_posts_by_tag},
|
||||||
|
},
|
||||||
|
projects::featured_projects::get_featured_projects,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::post_list::PostListTemplate;
|
||||||
|
|
||||||
|
// TODO Refactor to render post list for the same broadcasts, blog and cookbook
|
||||||
|
pub async fn render_blog_post_list(
|
||||||
|
tag: Option<Path<String>>,
|
||||||
|
OriginalUri(original_uri): OriginalUri,
|
||||||
|
) -> Result<PostListTemplate, StatusCode> {
|
||||||
|
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
||||||
|
let tag = tag.map(|Path(tag)| tag);
|
||||||
|
|
||||||
|
let (blog_tags, featured_projects, post_list) = try_join!(
|
||||||
|
get_popular_tags(Some(Segment::Blog)),
|
||||||
|
get_featured_projects(),
|
||||||
|
get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let posts = get_posts_by_segment(post_list, &[Segment::Blog]);
|
||||||
|
let posts = get_posts_by_tag(posts, &tag);
|
||||||
|
let header_props = match tag {
|
||||||
|
Some(_) => HeaderProps::with_back_link(Link {
|
||||||
|
href: "/blog".to_string(),
|
||||||
|
label: "All posts".to_string(),
|
||||||
|
}),
|
||||||
|
None => HeaderProps::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("uri:{:?}", original_uri);
|
||||||
|
|
||||||
|
let (title, og_title) = if let Some(tag) = &tag {
|
||||||
|
(format!("#{tag}"), format!("{tag} blog posts"))
|
||||||
|
} else {
|
||||||
|
("Blog posts".to_string(), "Blog posts".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PostListTemplate {
|
||||||
|
title,
|
||||||
|
og_title,
|
||||||
|
segment: Segment::Blog,
|
||||||
|
posts,
|
||||||
|
header_props,
|
||||||
|
tags: blog_tags,
|
||||||
|
featured_projects,
|
||||||
|
current_url: original_uri.to_string(),
|
||||||
|
})
|
||||||
|
}
|
64
src/pages/blog_post_page.rs
Normal file
64
src/pages/blog_post_page.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use askama::Template;
|
||||||
|
use axum::extract::OriginalUri;
|
||||||
|
use axum::{extract::Path, http::StatusCode};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
use crate::blog_posts::blog_post_model::BLOG_POST_PATH;
|
||||||
|
use crate::{
|
||||||
|
blog_posts::blog_post_model::BlogPostMetadata, components::site_header::Link, filters,
|
||||||
|
post_utils::post_parser::parse_post,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::components::site_header::HeaderProps;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "blog_post.html")]
|
||||||
|
pub struct BlogPostTemplate {
|
||||||
|
pub title: String,
|
||||||
|
pub body: String,
|
||||||
|
pub date: DateTime<Utc>,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
pub segment: String,
|
||||||
|
pub header_props: HeaderProps,
|
||||||
|
pub slug: String,
|
||||||
|
pub thumbnail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_blog_post(
|
||||||
|
Path(post_id): Path<String>,
|
||||||
|
OriginalUri(original_uri): OriginalUri,
|
||||||
|
) -> Result<BlogPostTemplate, StatusCode> {
|
||||||
|
let path = format!("{}/{}.md", BLOG_POST_PATH, post_id);
|
||||||
|
let parse_post = parse_post::<BlogPostMetadata>(&path, true);
|
||||||
|
let parsed = parse_post.await?;
|
||||||
|
let segment = if original_uri.to_string().starts_with("/blog") {
|
||||||
|
"blog"
|
||||||
|
} else if original_uri.to_string().starts_with("/broadcasts") {
|
||||||
|
"broadcasts"
|
||||||
|
} else {
|
||||||
|
"blog"
|
||||||
|
};
|
||||||
|
|
||||||
|
let header_props = match segment {
|
||||||
|
"blog" => HeaderProps::with_back_link(Link {
|
||||||
|
href: "/blog".to_string(),
|
||||||
|
label: "All posts".to_string(),
|
||||||
|
}),
|
||||||
|
"broadcasts" => HeaderProps::with_back_link(Link {
|
||||||
|
href: "/broadcasts".to_string(),
|
||||||
|
label: "All broadcasts".to_string(),
|
||||||
|
}),
|
||||||
|
_ => HeaderProps::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(BlogPostTemplate {
|
||||||
|
title: parsed.metadata.title,
|
||||||
|
date: parsed.metadata.date,
|
||||||
|
tags: parsed.metadata.tags,
|
||||||
|
body: parsed.body,
|
||||||
|
slug: parsed.slug,
|
||||||
|
segment: segment.to_string(),
|
||||||
|
thumbnail: parsed.metadata.thumbnail,
|
||||||
|
header_props,
|
||||||
|
})
|
||||||
|
}
|
63
src/pages/broadcast_list.rs
Normal file
63
src/pages/broadcast_list.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{OriginalUri, Path},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use tokio::try_join;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
blog_posts::blog_post_model::{BlogPostMetadata, Segment, BLOG_POST_PATH},
|
||||||
|
components::site_header::{HeaderProps, Link},
|
||||||
|
post_utils::{
|
||||||
|
post_listing::get_post_list,
|
||||||
|
segments::get_posts_by_segment,
|
||||||
|
tags::{get_popular_tags, get_posts_by_tag},
|
||||||
|
},
|
||||||
|
projects::featured_projects::get_featured_projects,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::post_list::PostListTemplate;
|
||||||
|
|
||||||
|
pub async fn render_broadcast_post_list(
|
||||||
|
tag: Option<Path<String>>,
|
||||||
|
OriginalUri(original_uri): OriginalUri,
|
||||||
|
) -> Result<PostListTemplate, StatusCode> {
|
||||||
|
// I will forget what happens here in a week. But essentially it's pattern matching and shadowing
|
||||||
|
let tag = tag.map(|Path(tag)| tag);
|
||||||
|
|
||||||
|
let (popular_tags, featured_projects, post_list) = try_join!(
|
||||||
|
get_popular_tags(Some(Segment::Broadcasts)),
|
||||||
|
get_featured_projects(),
|
||||||
|
get_post_list::<BlogPostMetadata>(BLOG_POST_PATH)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let posts = get_posts_by_segment(post_list, &[Segment::Broadcasts]);
|
||||||
|
let posts = get_posts_by_tag(posts, &tag);
|
||||||
|
|
||||||
|
let header_props = match tag {
|
||||||
|
Some(_) => HeaderProps::with_back_link(Link {
|
||||||
|
href: "/broadcasts".to_string(),
|
||||||
|
label: "All broadcasts".to_string(),
|
||||||
|
}),
|
||||||
|
None => HeaderProps::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("uri:{:?}", original_uri);
|
||||||
|
|
||||||
|
let title = if let Some(tag) = &tag {
|
||||||
|
format!("#{tag} broadcasts")
|
||||||
|
} else {
|
||||||
|
"Broadcasts".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PostListTemplate {
|
||||||
|
title: title.clone(),
|
||||||
|
og_title: title,
|
||||||
|
segment: Segment::Broadcasts,
|
||||||
|
posts,
|
||||||
|
header_props,
|
||||||
|
tags: popular_tags,
|
||||||
|
featured_projects,
|
||||||
|
current_url: original_uri.to_string(),
|
||||||
|
})
|
||||||
|
}
|
55
src/pages/index.rs
Normal file
55
src/pages/index.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use tokio::try_join;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
blog_posts::blog_post_model::{BlogPostMetadata, Segment, BLOG_POST_PATH},
|
||||||
|
components::site_header::HeaderProps,
|
||||||
|
filters,
|
||||||
|
post_utils::{
|
||||||
|
post_listing::get_post_list, post_parser::ParseResult, segments::ref_get_posts_by_segment,
|
||||||
|
tags::get_popular_tags,
|
||||||
|
},
|
||||||
|
projects::{featured_projects::get_featured_projects, project_model::ProjectMetadata},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "index.html")]
|
||||||
|
pub struct IndexTemplate {
|
||||||
|
header_props: HeaderProps,
|
||||||
|
blog_tags: Vec<String>,
|
||||||
|
broadcasts_tags: Vec<String>,
|
||||||
|
featured_blog_posts: Vec<Rc<ParseResult<BlogPostMetadata>>>,
|
||||||
|
featured_projects: Vec<ParseResult<ProjectMetadata>>,
|
||||||
|
featured_broadcasts: Vec<Rc<ParseResult<BlogPostMetadata>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_index() -> Result<IndexTemplate, StatusCode> {
|
||||||
|
let (blog_tags, broadcasts_tags, all_posts, featured_projects) = try_join!(
|
||||||
|
get_popular_tags(Some(Segment::Blog)),
|
||||||
|
get_popular_tags(Some(Segment::Broadcasts)),
|
||||||
|
get_post_list::<BlogPostMetadata>(BLOG_POST_PATH),
|
||||||
|
get_featured_projects()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Convert the all_posts into Rc<ParseResult<BlogPostMetadata>>
|
||||||
|
let all_posts_rc: Vec<Rc<ParseResult<BlogPostMetadata>>> =
|
||||||
|
all_posts.into_iter().map(Rc::new).collect();
|
||||||
|
|
||||||
|
let featured_blog_posts =
|
||||||
|
ref_get_posts_by_segment(&all_posts_rc, &[Segment::Blog, Segment::Featured]);
|
||||||
|
|
||||||
|
let featured_broadcasts =
|
||||||
|
ref_get_posts_by_segment(&all_posts_rc, &[Segment::Broadcasts, Segment::Featured]);
|
||||||
|
|
||||||
|
Ok(IndexTemplate {
|
||||||
|
header_props: HeaderProps::default(),
|
||||||
|
blog_tags,
|
||||||
|
broadcasts_tags,
|
||||||
|
featured_blog_posts,
|
||||||
|
featured_projects,
|
||||||
|
featured_broadcasts,
|
||||||
|
})
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
pub mod admin;
|
pub mod admin;
|
||||||
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 contact;
|
pub mod contact;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
|
pub mod not_found;
|
||||||
|
pub mod post_list;
|
||||||
pub mod project_list;
|
pub mod project_list;
|
||||||
|
pub mod showcase;
|
25
src/pages/not_found.rs
Normal file
25
src/pages/not_found.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use askama::Template;
|
||||||
|
use axum::{extract::OriginalUri, http::StatusCode};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::components::site_header::HeaderProps;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "not_found.html")]
|
||||||
|
pub struct NotFoundPage {
|
||||||
|
title: String,
|
||||||
|
header_props: HeaderProps,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_not_found(
|
||||||
|
OriginalUri(original_uri): OriginalUri,
|
||||||
|
) -> Result<(StatusCode, NotFoundPage), StatusCode> {
|
||||||
|
info!("{original_uri} not found");
|
||||||
|
Ok((
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
NotFoundPage {
|
||||||
|
title: "This page does not exists".to_owned(),
|
||||||
|
header_props: HeaderProps::default(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
22
src/pages/post_list.rs
Normal file
22
src/pages/post_list.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use askama::Template;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
blog_posts::blog_post_model::{BlogPostMetadata, Segment},
|
||||||
|
components::site_header::HeaderProps,
|
||||||
|
filters,
|
||||||
|
post_utils::post_parser::ParseResult,
|
||||||
|
projects::project_model::ProjectMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "post_list.html")]
|
||||||
|
pub struct PostListTemplate {
|
||||||
|
pub title: String,
|
||||||
|
pub og_title: String,
|
||||||
|
pub segment: Segment,
|
||||||
|
pub posts: Vec<ParseResult<BlogPostMetadata>>,
|
||||||
|
pub header_props: HeaderProps,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
pub featured_projects: Vec<ParseResult<ProjectMetadata>>,
|
||||||
|
pub current_url: String,
|
||||||
|
}
|
@ -16,7 +16,7 @@ pub struct ProjectListTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_projects_list() -> Result<ProjectListTemplate, StatusCode> {
|
pub async fn render_projects_list() -> Result<ProjectListTemplate, StatusCode> {
|
||||||
let mut project_list = get_post_list::<ProjectMetadata>("../_projects").await?;
|
let mut project_list = get_post_list::<ProjectMetadata>("_projects").await?;
|
||||||
|
|
||||||
project_list.sort_by_key(|post| post.slug.to_string());
|
project_list.sort_by_key(|post| post.slug.to_string());
|
||||||
project_list.retain(|project| project.metadata.displayed);
|
project_list.retain(|project| project.metadata.displayed);
|
16
src/pages/showcase/egg_fetcher.rs
Normal file
16
src/pages/showcase/egg_fetcher.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use askama::Template;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
|
||||||
|
use crate::components::site_header::HeaderProps;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "egg_fetcher_page.html")]
|
||||||
|
pub struct EggFetcherShowcaseTemplate {
|
||||||
|
header_props: HeaderProps,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn render_egg_fetcher() -> Result<EggFetcherShowcaseTemplate, StatusCode> {
|
||||||
|
Ok(EggFetcherShowcaseTemplate {
|
||||||
|
header_props: HeaderProps::default(),
|
||||||
|
})
|
||||||
|
}
|
1
src/pages/showcase/mod.rs
Normal file
1
src/pages/showcase/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod egg_fetcher;
|
@ -1,4 +0,0 @@
|
|||||||
/** @type {import('@sveltejs/kit').ParamMatcher} */
|
|
||||||
export function match(param: string) {
|
|
||||||
return !['tags', 'page'].some((keyword) => param.startsWith(keyword))
|
|
||||||
}
|
|
62
src/picture_generator/image_src_generator.rs
Normal file
62
src/picture_generator/image_src_generator.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use image::ImageReader;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
image_generator::generate_images,
|
||||||
|
picture_markup_generator::{get_export_formats, get_generated_file_name, get_image_path},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn generate_image_with_src(
|
||||||
|
orig_img_path: &str,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
suffix: &str,
|
||||||
|
generate_image: bool,
|
||||||
|
) -> Result<String, anyhow::Error> {
|
||||||
|
let path_to_generated = get_generated_file_name(orig_img_path);
|
||||||
|
let file_stem = path_to_generated.file_stem().unwrap().to_str().unwrap();
|
||||||
|
let path_to_generated = path_to_generated.with_file_name(format!("{file_stem}{suffix}"));
|
||||||
|
|
||||||
|
let disk_img_path =
|
||||||
|
Path::new("static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
|
||||||
|
let resolutions = [(width, height, 1.)];
|
||||||
|
|
||||||
|
let exported_formats = get_export_formats(orig_img_path);
|
||||||
|
let exported_format = *exported_formats.first().unwrap();
|
||||||
|
|
||||||
|
let path_to_generated_arc = Arc::new(path_to_generated);
|
||||||
|
let path_to_generated_clone = Arc::clone(&path_to_generated_arc);
|
||||||
|
|
||||||
|
if generate_image {
|
||||||
|
rayon::spawn(move || {
|
||||||
|
let orig_img = ImageReader::open(&disk_img_path)
|
||||||
|
.with_context(|| format!("Failed to read instrs from {:?}", &disk_img_path))
|
||||||
|
.unwrap()
|
||||||
|
.decode()
|
||||||
|
.unwrap();
|
||||||
|
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 path_to_generated = Arc::clone(&path_to_generated_arc);
|
||||||
|
|
||||||
|
let image_path = get_image_path(
|
||||||
|
&path_to_generated,
|
||||||
|
resolutions.first().expect("Should this error ever happen?"),
|
||||||
|
&exported_format,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(image_path)
|
||||||
|
}
|
@ -23,5 +23,6 @@ It should be used from the templates as well
|
|||||||
|
|
||||||
pub mod export_format;
|
pub mod export_format;
|
||||||
pub mod image_generator;
|
pub mod image_generator;
|
||||||
|
pub mod image_src_generator;
|
||||||
pub mod picture_markup_generator;
|
pub mod picture_markup_generator;
|
||||||
pub mod resolutions;
|
pub mod resolutions;
|
@ -42,9 +42,8 @@ pub fn generate_picture_markup(
|
|||||||
}
|
}
|
||||||
let path_to_generated = get_generated_file_name(orig_img_path);
|
let path_to_generated = get_generated_file_name(orig_img_path);
|
||||||
|
|
||||||
// TODO This should get removed when we move the project structure #directory-swap
|
|
||||||
let disk_img_path =
|
let disk_img_path =
|
||||||
Path::new("../static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
|
Path::new("static/").join(orig_img_path.strip_prefix("/").unwrap_or(orig_img_path));
|
||||||
|
|
||||||
let orig_img_dimensions = image_dimensions(&disk_img_path)?;
|
let orig_img_dimensions = image_dimensions(&disk_img_path)?;
|
||||||
let resolutions = get_resolutions(orig_img_dimensions, width, height);
|
let resolutions = get_resolutions(orig_img_dimensions, width, height);
|
||||||
@ -120,7 +119,7 @@ pub fn generate_picture_markup(
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_image_path(path: &Path, resolution: &(u32, u32, f32), format: &ExportFormat) -> String {
|
pub fn get_image_path(path: &Path, resolution: &(u32, u32, f32), format: &ExportFormat) -> String {
|
||||||
let path_name = path.to_str().expect("Image has to have a valid path");
|
let path_name = path.to_str().expect("Image has to have a valid path");
|
||||||
let (width, height, _) = resolution;
|
let (width, height, _) = resolution;
|
||||||
let extension = format.get_extension();
|
let extension = format.get_extension();
|
||||||
@ -198,7 +197,7 @@ fn strip_prefixes(path: &Path) -> &Path {
|
|||||||
parent_path
|
parent_path
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_generated_file_name(orig_img_path: &str) -> PathBuf {
|
pub fn get_generated_file_name(orig_img_path: &str) -> PathBuf {
|
||||||
let path = Path::new(&orig_img_path);
|
let path = Path::new(&orig_img_path);
|
||||||
// let parent = path
|
// let parent = path
|
||||||
// .parent()
|
// .parent()
|
||||||
@ -248,7 +247,7 @@ fn generate_srcset(path: &Path, format: &ExportFormat, resolutions: &[(u32, u32,
|
|||||||
.join(", ")
|
.join(", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> {
|
pub fn get_export_formats(orig_img_path: &str) -> Vec<ExportFormat> {
|
||||||
let path = Path::new(&orig_img_path)
|
let path = Path::new(&orig_img_path)
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|ext| ext.to_str());
|
.and_then(|ext| ext.to_str());
|
@ -1,2 +1,4 @@
|
|||||||
pub mod post_listing;
|
pub mod post_listing;
|
||||||
pub mod post_parser;
|
pub mod post_parser;
|
||||||
|
pub mod segments;
|
||||||
|
pub mod tags;
|
@ -8,7 +8,6 @@ use super::post_parser::{parse_post, ParseResult};
|
|||||||
pub async fn get_post_list<'de, Metadata: DeserializeOwned>(
|
pub async fn get_post_list<'de, Metadata: DeserializeOwned>(
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<Vec<ParseResult<Metadata>>, StatusCode> {
|
) -> Result<Vec<ParseResult<Metadata>>, StatusCode> {
|
||||||
// let path = "../_posts/blog/";
|
|
||||||
let mut dir = read_dir(path)
|
let mut dir = read_dir(path)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
@ -10,7 +10,7 @@ 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 syntect::{highlighting::ThemeSet, html::highlighted_html_for_string, parsing::SyntaxSet};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::picture_generator::{
|
use crate::picture_generator::{
|
||||||
picture_markup_generator::generate_picture_markup, resolutions::get_max_resolution,
|
picture_markup_generator::generate_picture_markup, resolutions::get_max_resolution,
|
||||||
@ -32,6 +32,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ParseResult<Metadata> {
|
pub struct ParseResult<Metadata> {
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub metadata: Metadata,
|
pub metadata: Metadata,
|
||||||
@ -117,7 +118,7 @@ pub fn parse_html(markdown: &str, generate_images: bool) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let dev_only_img_path =
|
let dev_only_img_path =
|
||||||
Path::new("../static/").join(dest_url.strip_prefix("/").unwrap_or(&dest_url));
|
Path::new("static/").join(dest_url.strip_prefix("/").unwrap_or(&dest_url));
|
||||||
let img_dimensions = image_dimensions(&dev_only_img_path).unwrap();
|
let img_dimensions = image_dimensions(&dev_only_img_path).unwrap();
|
||||||
|
|
||||||
let (max_width, max_height) = get_max_resolution(
|
let (max_width, max_height) = get_max_resolution(
|
66
src/post_utils/segments.rs
Normal file
66
src/post_utils/segments.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
use std::{fmt::Display, rc::Rc};
|
||||||
|
|
||||||
|
use crate::blog_posts::blog_post_model::{BlogPostMetadata, Segment};
|
||||||
|
|
||||||
|
use super::post_parser::ParseResult;
|
||||||
|
|
||||||
|
impl Segment {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Segment::Blog => "blog",
|
||||||
|
Segment::Broadcasts => "broadcasts",
|
||||||
|
Segment::Featured => "featured",
|
||||||
|
Segment::Cookbook => "cookbook",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Segment {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_posts_by_segment(
|
||||||
|
post_list: Vec<ParseResult<BlogPostMetadata>>,
|
||||||
|
segments: &[Segment],
|
||||||
|
) -> Vec<ParseResult<BlogPostMetadata>> {
|
||||||
|
let mut filtered_posts: Vec<ParseResult<BlogPostMetadata>> = post_list
|
||||||
|
.into_iter()
|
||||||
|
.filter(|post| {
|
||||||
|
segments
|
||||||
|
.iter()
|
||||||
|
.all(|segment| post.metadata.segments.contains(segment))
|
||||||
|
}) // Filter by segments
|
||||||
|
.filter(|post| post.metadata.published) // Filter only published posts
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Sort by date in descending order
|
||||||
|
filtered_posts.sort_by_key(|post| post.metadata.date);
|
||||||
|
filtered_posts.reverse();
|
||||||
|
|
||||||
|
filtered_posts
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ref_get_posts_by_segment(
|
||||||
|
post_list: &[Rc<ParseResult<BlogPostMetadata>>],
|
||||||
|
segments: &[Segment],
|
||||||
|
) -> Vec<Rc<ParseResult<BlogPostMetadata>>> {
|
||||||
|
let mut filtered_posts: Vec<Rc<ParseResult<BlogPostMetadata>>> = post_list
|
||||||
|
.iter() // Use iter() to borrow instead of consuming the original vector
|
||||||
|
.filter(|post| {
|
||||||
|
let post = post.as_ref();
|
||||||
|
segments
|
||||||
|
.iter()
|
||||||
|
.all(|segment| post.metadata.segments.contains(segment))
|
||||||
|
}) // Filter by segments
|
||||||
|
.filter(|post| post.metadata.published) // Filter only published posts
|
||||||
|
.cloned()
|
||||||
|
.collect(); // Collect references to ParseResult<BlogPostMetadata>
|
||||||
|
|
||||||
|
// Sort by date in descending order
|
||||||
|
filtered_posts.sort_by_key(|post| post.metadata.date);
|
||||||
|
filtered_posts.reverse();
|
||||||
|
|
||||||
|
filtered_posts
|
||||||
|
}
|
63
src/post_utils/tags.rs
Normal file
63
src/post_utils/tags.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use axum::http::StatusCode;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::blog_posts::blog_post_model::{BlogPostMetadata, Segment, BLOG_POST_PATH};
|
||||||
|
|
||||||
|
use super::{post_listing::get_post_list, post_parser::ParseResult};
|
||||||
|
|
||||||
|
pub async fn get_popular_tags(segment: Option<Segment>) -> Result<Vec<String>, StatusCode> {
|
||||||
|
const TAGS_LENGTH: usize = 7;
|
||||||
|
|
||||||
|
let mut post_list = get_post_list::<BlogPostMetadata>(BLOG_POST_PATH).await?;
|
||||||
|
post_list.retain(|post| post.metadata.published);
|
||||||
|
|
||||||
|
if let Some(segment) = segment {
|
||||||
|
post_list.retain(|post| post.metadata.segments.contains(&segment));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags_sum = post_list
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|post| post.metadata.tags)
|
||||||
|
.fold(HashMap::new(), |mut acc, tag| {
|
||||||
|
*acc.entry(tag).or_insert(0) += 1;
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut sorted_tags_by_count: Vec<_> = tags_sum.into_iter().collect();
|
||||||
|
sorted_tags_by_count.sort_by_key(|&(_, count)| std::cmp::Reverse(count));
|
||||||
|
|
||||||
|
// Log the counts
|
||||||
|
for (tag, count) in &sorted_tags_by_count {
|
||||||
|
debug!("Tag: {}, Count: {}", tag, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
let popular_tags = sorted_tags_by_count
|
||||||
|
.into_iter()
|
||||||
|
.map(|tag_count| tag_count.0)
|
||||||
|
.filter(|tag| tag != "dev")
|
||||||
|
.take(TAGS_LENGTH)
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
Ok(popular_tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_posts_by_tag(
|
||||||
|
post_list: Vec<ParseResult<BlogPostMetadata>>,
|
||||||
|
tag: &Option<String>,
|
||||||
|
) -> Vec<ParseResult<BlogPostMetadata>> {
|
||||||
|
match tag {
|
||||||
|
Some(tag) => post_list
|
||||||
|
.into_iter()
|
||||||
|
.filter(|post| {
|
||||||
|
post.metadata
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.map(|post_tag| post_tag.to_lowercase())
|
||||||
|
.collect::<String>()
|
||||||
|
.contains(&tag.to_lowercase())
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
None => post_list,
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ use crate::post_utils::{post_listing::get_post_list, post_parser::ParseResult};
|
|||||||
use super::project_model::ProjectMetadata;
|
use super::project_model::ProjectMetadata;
|
||||||
|
|
||||||
pub async fn get_featured_projects() -> Result<Vec<ParseResult<ProjectMetadata>>, StatusCode> {
|
pub async fn get_featured_projects() -> Result<Vec<ParseResult<ProjectMetadata>>, StatusCode> {
|
||||||
let project_list = get_post_list::<ProjectMetadata>("../_projects").await?;
|
let project_list = get_post_list::<ProjectMetadata>("_projects").await?;
|
||||||
|
|
||||||
let featured_projects = project_list
|
let featured_projects = project_list
|
||||||
.into_iter()
|
.into_iter()
|
@ -2,8 +2,9 @@ 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, blog_post_list::render_blog_post_list,
|
||||||
blog_post_page::render_blog_post, contact::render_contact, index::render_index,
|
blog_post_page::render_blog_post, broadcast_list::render_broadcast_post_list,
|
||||||
project_list::render_projects_list,
|
contact::render_contact, index::render_index, not_found::render_not_found,
|
||||||
|
project_list::render_projects_list, showcase::egg_fetcher::render_egg_fetcher,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
|
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
|
||||||
@ -16,8 +17,12 @@ pub fn get_router() -> Router {
|
|||||||
.route("/blog", get(render_blog_post_list))
|
.route("/blog", get(render_blog_post_list))
|
||||||
.route("/blog/tags/:tag", get(render_blog_post_list))
|
.route("/blog/tags/:tag", get(render_blog_post_list))
|
||||||
.route("/blog/:post_id", get(render_blog_post))
|
.route("/blog/:post_id", get(render_blog_post))
|
||||||
|
.route("/broadcasts", get(render_broadcast_post_list))
|
||||||
|
.route("/broadcasts/tags/:tag", get(render_broadcast_post_list))
|
||||||
|
.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/:project_slug", get(render_egg_fetcher))
|
||||||
.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(
|
||||||
@ -37,4 +42,5 @@ pub fn get_router() -> Router {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.fallback(render_not_found)
|
||||||
}
|
}
|
@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { LayoutData } from './$types'
|
|
||||||
import Nav from '$lib/components/Nav.svelte'
|
|
||||||
import Footer from '$lib/components/Footer.svelte'
|
|
||||||
import 'modern-normalize/modern-normalize.css'
|
|
||||||
import '$lib/styles/global.css'
|
|
||||||
import { mainContentClass } from './layout.css'
|
|
||||||
|
|
||||||
export let data: LayoutData
|
|
||||||
export let latestPosts = data.latestPosts
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="app-content">
|
|
||||||
<Nav />
|
|
||||||
|
|
||||||
<main class={mainContentClass}>
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
<Footer {latestPosts} />
|
|
||||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||||||
import type { LayoutLoad } from './$types'
|
|
||||||
export const prerender = true
|
|
||||||
|
|
||||||
export const load = (async ({ fetch }) => {
|
|
||||||
const blogPostsResponse = await fetch(`/articles/pageSize/5.json`)
|
|
||||||
const blogPostsContent = await blogPostsResponse.json()
|
|
||||||
|
|
||||||
return {
|
|
||||||
latestPosts: blogPostsContent.posts.items,
|
|
||||||
}
|
|
||||||
}) satisfies LayoutLoad
|
|
@ -1,83 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { generateSrcSet, getNFResize } from '$lib/large-media'
|
|
||||||
import {
|
|
||||||
citeOwnerClass,
|
|
||||||
mottoClass,
|
|
||||||
profilePicClass,
|
|
||||||
profilePicImgClass,
|
|
||||||
twitchAsideClass,
|
|
||||||
twitchEmbedClass,
|
|
||||||
twitchIframeClass,
|
|
||||||
twitchStreamPromoClass,
|
|
||||||
welcomeNoteClass,
|
|
||||||
} from './page.css'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Introduction @michalvankodev</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<header class="index-header">
|
|
||||||
<figure class="profile-pic {profilePicClass}">
|
|
||||||
<picture>
|
|
||||||
<img
|
|
||||||
alt="Portrait"
|
|
||||||
class="{profilePicImgClass}"
|
|
||||||
srcset={generateSrcSet('/images/profile-portugal-landscape.jpg', {
|
|
||||||
width: 800,
|
|
||||||
})}
|
|
||||||
src={getNFResize('/images/profile-portugal-landscape.jpg', {
|
|
||||||
width: 800,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</picture>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<p class="motto {mottoClass}">
|
|
||||||
<cite>“Let your ambition carry you.”</cite>
|
|
||||||
<span class="cite-owner {citeOwnerClass}">- La Flame</span>
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p class={welcomeNoteClass}>
|
|
||||||
Hey, welcome to my personal website. My name is
|
|
||||||
<strong>Michal Vanko</strong>
|
|
||||||
and I'm a
|
|
||||||
<em> <a href="https://en.wikipedia.org/wiki/Programmer">programmer</a> </em>
|
|
||||||
. I'll try to share some stories and opinions about things that I'm interested
|
|
||||||
in.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<section class="twitch-stream-promo {twitchStreamPromoClass}">
|
|
||||||
<h2>Follow my twitch stream</h2>
|
|
||||||
<div class="twitch-embed {twitchEmbedClass}">
|
|
||||||
<div class="twitch-video {twitchIframeClass}">
|
|
||||||
<iframe
|
|
||||||
title="My twitch channel"
|
|
||||||
src="https://player.twitch.tv/?channel=michalvankodev&parent=michalvanko.dev&parent=localhost&autoplay=false"
|
|
||||||
loading="lazy"
|
|
||||||
frameborder="0"
|
|
||||||
scrolling="no"
|
|
||||||
allowfullscreen
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
class="embed {twitchIframeClass}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<aside class={twitchAsideClass}>
|
|
||||||
Come hang out and chat with me <strong>every Tuesday and Thursday</strong>
|
|
||||||
afternoon central Europe time. I stream working on my side-projects and talking
|
|
||||||
anything about the developer lifestyle.
|
|
||||||
</aside>
|
|
||||||
<!-- <div class="twitch-chat">
|
|
||||||
<iframe
|
|
||||||
title="Twitch chat"
|
|
||||||
frameborder="0"
|
|
||||||
scrolling="no"
|
|
||||||
src="https://www.twitch.tv/embed/michalvankodev/chat?parent=michalvanko.dev&parent=localhost"
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</section>
|
|
@ -1,23 +0,0 @@
|
|||||||
import {
|
|
||||||
getDropTakeFromPageParams,
|
|
||||||
parseParams,
|
|
||||||
} from '$lib/pagination/dropTakeParams'
|
|
||||||
import { json } from '@sveltejs/kit'
|
|
||||||
import { getBlogListing } from '$lib/articleContent/articleContentListing'
|
|
||||||
import type { RequestHandler } from './$types'
|
|
||||||
|
|
||||||
export const prerender = true
|
|
||||||
export const GET = (async ({ params }) => {
|
|
||||||
const handledParams = params.params === 'index' ? '' : params.params
|
|
||||||
const { page = 1, pageSize = 7, ...filters } = parseParams(handledParams)
|
|
||||||
const paginationParams = getDropTakeFromPageParams(
|
|
||||||
Number(pageSize),
|
|
||||||
Number(page)
|
|
||||||
)
|
|
||||||
const paginationQuery = { ...paginationParams, filters }
|
|
||||||
const filteredContents = await getBlogListing(paginationQuery)
|
|
||||||
|
|
||||||
return json({
|
|
||||||
posts: filteredContents,
|
|
||||||
})
|
|
||||||
}) satisfies RequestHandler
|
|
@ -1,32 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { PageData } from './$types'
|
|
||||||
import ArticlePreviewList from '$lib/components/articles/ArticlePreviewList/ArticlePreviewList.svelte'
|
|
||||||
import { seeAllClass } from '$lib/components/articles/ArticlePreviewList/ArticlePreviewList.css'
|
|
||||||
|
|
||||||
export let data: PageData
|
|
||||||
$: ({ posts, filters } = data)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>My blog @michalvankodev</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
{#if posts.items.length === 0}
|
|
||||||
<p class="no-posts">You've found void in the space.</p>
|
|
||||||
{:else}
|
|
||||||
<h1>
|
|
||||||
{#if filters.tags}
|
|
||||||
<em>{filters.tags}</em>
|
|
||||||
{:else}
|
|
||||||
Blog
|
|
||||||
{/if}
|
|
||||||
posts
|
|
||||||
</h1>
|
|
||||||
{#if filters.tags}
|
|
||||||
<div class={seeAllClass}>
|
|
||||||
<a href="/blog">See all posts</a>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<ArticlePreviewList {...data} segment="blog" />
|
|
@ -1,17 +0,0 @@
|
|||||||
import { parseParams } from '$lib/pagination/dropTakeParams'
|
|
||||||
import type { PageLoad } from './$types'
|
|
||||||
import type { ArticlePreviewAttributes } from '$lib/articleContent/articleContentListing'
|
|
||||||
import type { PaginationResult } from '$lib/pagination/pagination'
|
|
||||||
|
|
||||||
export const load = (async ({ fetch, params }) => {
|
|
||||||
const { page = 1, pageSize = 7, ...filters } = parseParams(params.params)
|
|
||||||
const articleResponse = await fetch(
|
|
||||||
`/articles/segments/blog${params.params ? `/${params.params}` : ''}.json`
|
|
||||||
).then((r) => r.json())
|
|
||||||
return {
|
|
||||||
posts: articleResponse.posts as PaginationResult<ArticlePreviewAttributes >,
|
|
||||||
page: Number(page),
|
|
||||||
pageSize,
|
|
||||||
filters,
|
|
||||||
}
|
|
||||||
}) satisfies PageLoad
|
|
@ -1,9 +0,0 @@
|
|||||||
import type { PageServerLoad } from './$types'
|
|
||||||
import { getArticleContent } from '$lib/articleContent/articleContent'
|
|
||||||
|
|
||||||
export const prerender = true
|
|
||||||
|
|
||||||
export const load = (async ({ params: { slug } }) => {
|
|
||||||
const post = await getArticleContent(slug);
|
|
||||||
return post
|
|
||||||
}) satisfies PageServerLoad
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user