Compare commits

...

19 Commits

Author SHA1 Message Date
4aa64a880d style: fix linting complaints 2026-04-02 18:43:51 +02:00
614a32e0d4 feat: add PGP fingerprint 2026-04-02 18:33:53 +02:00
59525cbde5 feat: mention astro in web dev frameworks 2026-04-02 18:28:16 +02:00
264fe63e9b feat: expand descriptive paragraph 2026-04-02 18:26:20 +02:00
7d4c8e6cb1 feat: add a link to blecon.de 2026-04-02 18:22:44 +02:00
924af6e200 feat: put link to skills in separate nav 2026-04-02 17:54:58 +02:00
62339b4f33 feat: refactor inline links and use heroicons 2026-04-01 21:03:52 +02:00
d9d87daf7e feat: make InlineSvg more universal 2026-04-01 20:34:37 +02:00
320035c828 fix: pass through rest props in ButtonLink 2026-04-01 20:12:48 +02:00
92ca1f6feb refactor: rename ExternalLink to ButtonLink 2026-04-01 20:05:33 +02:00
bfd53b66ec refactor: ExternalLink component properties 2026-04-01 20:03:20 +02:00
d6aea329fe style: add prettier-plugin-organize-imports 2026-04-01 19:48:28 +02:00
d5eef87740 feat: set proper rel-attributes to external links 2026-04-01 19:16:12 +02:00
6eb4334691 feat: set up a good looking outline for links 2026-04-01 19:15:40 +02:00
4ce0421e95 chore(astro): update 2026-04-01 18:13:26 +02:00
91c8a825be feat(skills): auto-calc years experience and add back arrow 2026-03-28 22:27:02 +01:00
e5ab74ebfb feat: add a first version of the skills page 2026-03-26 22:07:00 +01:00
3e7c1d7ad3 feat: add a base layout for base HTML 2026-03-26 19:48:16 +01:00
2b988b9be9 build: update tooling configs and ignore files 2026-03-24 17:20:35 +01:00
21 changed files with 544 additions and 140 deletions

View File

@@ -2,5 +2,8 @@
"extends": ["markuplint:recommended-static-html"], "extends": ["markuplint:recommended-static-html"],
"parser": { "parser": {
".astro$": "@markuplint/astro-parser" ".astro$": "@markuplint/astro-parser"
},
"rules": {
"use-list": false
} }
} }

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
/.astro/
/dist/
/pnpm-lock.yaml

View File

@@ -1,7 +1,18 @@
# vim: filetype=gitignore # vim: filetype=gitignore
/dist/
*.html
*.ico *.ico
*.jpg
*.js
*.json *.json
*.md *.md
*.mjs *.mjs
*.mts
*.png
*.svg *.svg
*.ts
*.vim
*.webp
*.yaml *.yaml

View File

@@ -31,9 +31,9 @@ Any static assets, like images, can be placed in the `public/` directory.
All commands are run from the root of the project, from a terminal: All commands are run from the root of the project, from a terminal:
| Command | Action | | Command | Action |
| :------------------------ | :----------------------------------------------- | | :--------------------- | :----------------------------------------------- |
| `pnpm install` | Installs dependencies | | `pnpm install` | Installs dependencies |
| `pnpm dev` | Starts local dev server at `localhost:4321` | | `pnpm dev` | Starts local dev server at `localhost:4321` |
| `pnpm build` | Build your production site to `./dist/` | | `pnpm build` | Build your production site to `./dist/` |
| `pnpm preview` | Preview your build locally, before deploying | | `pnpm preview` | Preview your build locally, before deploying |

View File

@@ -1,6 +1,6 @@
// @ts-check // @ts-check
import { defineConfig } from "astro/config";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "astro/config";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({

View File

@@ -1,21 +1,27 @@
import js from "@eslint/js"; import js from "@eslint/js";
import eslintPluginAstro from "eslint-plugin-astro"; import eslintPluginAstro from "eslint-plugin-astro";
import jsxA11y from "eslint-plugin-jsx-a11y";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import { defineConfig } from "eslint/config"; import { defineConfig } from "eslint/config";
import jsxA11y from "eslint-plugin-jsx-a11y";
export default defineConfig([ export default defineConfig([
{
ignores: ["./dist/", "**/*.html"],
},
{ {
files: ["**/*.{js,mjs}"], files: ["**/*.{js,mjs}"],
plugins: { js }, plugins: { js },
extends: ["js/recommended"], extends: ["js/recommended"],
}, },
{ {
files: ["**/*.{astro,html}"], files: ["**/*.{astro,html}"],
plugins: { plugins: {
"jsx-a11y": jsxA11y, "jsx-a11y": jsxA11y,
}, },
}, },
...eslintPluginAstro.configs.recommended, ...eslintPluginAstro.configs.recommended,
eslintPluginPrettierRecommended, eslintPluginPrettierRecommended,
]); ]);

View File

@@ -7,14 +7,16 @@
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint:eslint": "eslint", "lint:eslint": "eslint",
"lint:markuplint": "markuplint '**/*.{astro,html}'", "lint:markuplint": "markuplint './{public,src}/**/*.{astro,html}'",
"lint:stylelint": "stylelint .", "lint:stylelint": "stylelint .",
"deploy": "astro build && scp -r dist/* vds:~/dockervolumes/kmoschcau_website/web/" "deploy": "astro build && scp -r dist/* vds:~/dockervolumes/kmoschcau_website/web/"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"astro": "^6.0.8", "astro": "^6.1.2",
"tailwindcss": "^4.2.2" "tailwindcss": "^4.2.2"
}, },
"devDependencies": { "devDependencies": {
@@ -23,6 +25,7 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@markuplint/astro-parser": "^4.6.23", "@markuplint/astro-parser": "^4.6.23",
"@markuplint/ml-config": "^4.8.15", "@markuplint/ml-config": "^4.8.15",
"@tailwindcss/typography": "^0.5.19",
"@typescript-eslint/parser": "^8.57.2", "@typescript-eslint/parser": "^8.57.2",
"cspell": "^9.7.0", "cspell": "^9.7.0",
"eslint": "^10.1.0", "eslint": "^10.1.0",
@@ -34,6 +37,7 @@
"postcss-html": "^1.8.1", "postcss-html": "^1.8.1",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1", "prettier-plugin-astro": "^0.14.1",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"stylelint": "^17.5.0", "stylelint": "^17.5.0",

64
pnpm-lock.yaml generated
View File

@@ -15,8 +15,8 @@ importers:
specifier: ^4.2.2 specifier: ^4.2.2
version: 4.2.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)) version: 4.2.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
astro: astro:
specifier: ^6.0.8 specifier: ^6.1.2
version: 6.0.8(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.3) version: 6.1.2(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.3)
tailwindcss: tailwindcss:
specifier: ^4.2.2 specifier: ^4.2.2
version: 4.2.2 version: 4.2.2
@@ -36,6 +36,9 @@ importers:
'@markuplint/ml-config': '@markuplint/ml-config':
specifier: ^4.8.15 specifier: ^4.8.15
version: 4.8.15 version: 4.8.15
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@4.2.2)
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^8.57.2 specifier: ^8.57.2
version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)
@@ -69,9 +72,12 @@ importers:
prettier-plugin-astro: prettier-plugin-astro:
specifier: ^0.14.1 specifier: ^0.14.1
version: 0.14.1 version: 0.14.1
prettier-plugin-organize-imports:
specifier: ^4.3.0
version: 4.3.0(prettier@3.8.1)(typescript@5.9.3)
prettier-plugin-tailwindcss: prettier-plugin-tailwindcss:
specifier: ^0.7.2 specifier: ^0.7.2
version: 0.7.2(prettier-plugin-astro@0.14.1)(prettier@3.8.1) version: 0.7.2(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.3.0(prettier@3.8.1)(typescript@5.9.3))(prettier@3.8.1)
sharp: sharp:
specifier: ^0.34.5 specifier: ^0.34.5
version: 0.34.5 version: 0.34.5
@@ -105,8 +111,8 @@ packages:
'@astrojs/internal-helpers@0.8.0': '@astrojs/internal-helpers@0.8.0':
resolution: {integrity: sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==} resolution: {integrity: sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==}
'@astrojs/markdown-remark@7.0.1': '@astrojs/markdown-remark@7.1.0':
resolution: {integrity: sha512-zAfLJmn07u9SlDNNHTpjv0RT4F8D4k54NR7ReRas8CO4OeGoqSvOuKwqCFg2/cqN3wHwdWlK/7Yv/lMXlhVIaw==} resolution: {integrity: sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ==}
'@astrojs/prism@4.0.1': '@astrojs/prism@4.0.1':
resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==} resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==}
@@ -1170,6 +1176,11 @@ packages:
resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
'@tailwindcss/typography@0.5.19':
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tailwindcss/vite@4.2.2': '@tailwindcss/vite@4.2.2':
resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==}
peerDependencies: peerDependencies:
@@ -1351,8 +1362,8 @@ packages:
resolution: {integrity: sha512-+QDcgc7e+au6EZ0YjMmRRjNoQo5bDMlaR45aWDoFsuxQTCM9qmCHRoiKJPELgckJ8Wmr7vcfpa9eCDHBFh6G4w==} resolution: {integrity: sha512-+QDcgc7e+au6EZ0YjMmRRjNoQo5bDMlaR45aWDoFsuxQTCM9qmCHRoiKJPELgckJ8Wmr7vcfpa9eCDHBFh6G4w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
astro@6.0.8: astro@6.1.2:
resolution: {integrity: sha512-DCPeb8GKOoFWh+8whB7Qi/kKWD/6NcQ9nd1QVNzJFxgHkea3WYrNroQRq4whmBdjhkYPTLS/1gmUAl2iA2Es2g==} resolution: {integrity: sha512-r3iIvmB6JvQxsdJLvapybKKq7Bojd1iQK6CCx5P55eRnXJIyUpHx/1UB/GdMm+em/lwaCUasxHCmIO0lCLV2uA==}
engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'}
hasBin: true hasBin: true
@@ -2904,6 +2915,10 @@ packages:
peerDependencies: peerDependencies:
postcss: ^8.4.31 postcss: ^8.4.31
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
postcss-selector-parser@7.1.1: postcss-selector-parser@7.1.1:
resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2927,6 +2942,16 @@ packages:
resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==} resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==}
engines: {node: ^14.15.0 || >=16.0.0} engines: {node: ^14.15.0 || >=16.0.0}
prettier-plugin-organize-imports@4.3.0:
resolution: {integrity: sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==}
peerDependencies:
prettier: '>=2.0'
typescript: '>=2.9'
vue-tsc: ^2.1.0 || 3
peerDependenciesMeta:
vue-tsc:
optional: true
prettier-plugin-tailwindcss@0.7.2: prettier-plugin-tailwindcss@0.7.2:
resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==}
engines: {node: '>=20.19'} engines: {node: '>=20.19'}
@@ -3654,7 +3679,7 @@ snapshots:
dependencies: dependencies:
picomatch: 4.0.4 picomatch: 4.0.4
'@astrojs/markdown-remark@7.0.1': '@astrojs/markdown-remark@7.1.0':
dependencies: dependencies:
'@astrojs/internal-helpers': 0.8.0 '@astrojs/internal-helpers': 0.8.0
'@astrojs/prism': 4.0.1 '@astrojs/prism': 4.0.1
@@ -3669,6 +3694,7 @@ snapshots:
remark-parse: 11.0.0 remark-parse: 11.0.0
remark-rehype: 11.1.2 remark-rehype: 11.1.2
remark-smartypants: 3.0.2 remark-smartypants: 3.0.2
retext-smartypants: 6.2.0
shiki: 4.0.2 shiki: 4.0.2
smol-toml: 1.6.1 smol-toml: 1.6.1
unified: 11.0.5 unified: 11.0.5
@@ -4619,6 +4645,11 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
'@tailwindcss/oxide-win32-x64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2
'@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)':
dependencies:
postcss-selector-parser: 6.0.10
tailwindcss: 4.2.2
'@tailwindcss/vite@4.2.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))': '@tailwindcss/vite@4.2.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
dependencies: dependencies:
'@tailwindcss/node': 4.2.2 '@tailwindcss/node': 4.2.2
@@ -4853,11 +4884,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
astro@6.0.8(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.3): astro@6.1.2(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies: dependencies:
'@astrojs/compiler': 3.0.1 '@astrojs/compiler': 3.0.1
'@astrojs/internal-helpers': 0.8.0 '@astrojs/internal-helpers': 0.8.0
'@astrojs/markdown-remark': 7.0.1 '@astrojs/markdown-remark': 7.1.0
'@astrojs/telemetry': 3.3.0 '@astrojs/telemetry': 3.3.0
'@capsizecss/unpack': 4.0.0 '@capsizecss/unpack': 4.0.0
'@clack/prompts': 1.1.0 '@clack/prompts': 1.1.0
@@ -6837,6 +6868,11 @@ snapshots:
dependencies: dependencies:
postcss: 8.5.8 postcss: 8.5.8
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-selector-parser@7.1.1: postcss-selector-parser@7.1.1:
dependencies: dependencies:
cssesc: 3.0.0 cssesc: 3.0.0
@@ -6862,11 +6898,17 @@ snapshots:
prettier: 3.8.1 prettier: 3.8.1
sass-formatter: 0.7.9 sass-formatter: 0.7.9
prettier-plugin-tailwindcss@0.7.2(prettier-plugin-astro@0.14.1)(prettier@3.8.1): prettier-plugin-organize-imports@4.3.0(prettier@3.8.1)(typescript@5.9.3):
dependencies:
prettier: 3.8.1
typescript: 5.9.3
prettier-plugin-tailwindcss@0.7.2(prettier-plugin-astro@0.14.1)(prettier-plugin-organize-imports@4.3.0(prettier@3.8.1)(typescript@5.9.3))(prettier@3.8.1):
dependencies: dependencies:
prettier: 3.8.1 prettier: 3.8.1
optionalDependencies: optionalDependencies:
prettier-plugin-astro: 0.14.1 prettier-plugin-astro: 0.14.1
prettier-plugin-organize-imports: 4.3.0(prettier@3.8.1)(typescript@5.9.3)
prettier@3.8.1: {} prettier@3.8.1: {}

View File

@@ -1,6 +1,10 @@
/** @type {import("prettier").Config} */ /** @type {import("prettier").Config} */
export default { export default {
plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"], plugins: [
"prettier-plugin-astro",
"prettier-plugin-organize-imports",
"prettier-plugin-tailwindcss",
],
overrides: [ overrides: [
{ {

View File

@@ -0,0 +1,21 @@
---
import type { HTMLAttributes } from "astro/types";
import { getRel } from "../link-utils";
interface Props extends HTMLAttributes<"a"> {
/** Whether this link should use defaults for external links. */
external?: boolean | "me" | null | undefined;
}
const { class: className, external, target, rel, ...rest } = Astro.props;
---
<a
class:list={[
className,
"flex min-w-full items-center justify-center gap-4 rounded px-6 py-2 text-lg outline-offset-2 sm:px-24",
]}
target={target ?? (external ? "_blank" : undefined)}
rel={rel ?? getRel(external)}
{...rest}><slot name="logo" /><slot /></a
>

View File

@@ -1,22 +0,0 @@
---
import type { HTMLAttributes } from "astro/types";
interface Props extends HTMLAttributes<"a"> {
cssClass: string;
href: string;
title: string;
}
const { cssClass, href, title } = Astro.props;
---
<a
class:list={[
cssClass,
"flex min-w-full items-center justify-center gap-4 rounded px-6 py-2 text-lg sm:px-24",
]}
{href}
{title}
target="_blank"
rel="external me noreferrer"><slot name="logo" /><slot /></a
>

View File

@@ -0,0 +1,24 @@
---
import type { HTMLAttributes } from "astro/types";
import ExternalIcon from "../icons/arrow-top-right-on-square.svg";
import InlineSvg from "./inline-svg.astro";
import { getRel } from "../link-utils";
interface Props extends HTMLAttributes<"a"> {
/** Whether this link should use defaults for external links. */
external?: boolean | "me" | null | undefined;
}
const { external, target, rel, ...rest } = Astro.props;
---
<a
target={target ?? (external ? "_blank" : undefined)}
rel={rel ?? getRel(external)}
{...rest}
><slot />{
external ? (
<InlineSvg SvgComponent={ExternalIcon} class="ms-1" />
) : undefined
}</a
>

View File

@@ -1,17 +1,16 @@
--- ---
import type { SvgComponent } from "astro/types"; import type { HTMLAttributes, SvgComponent } from "astro/types";
interface Props { interface Props extends HTMLAttributes<"svg"> {
SvgComponent: SvgComponent; SvgComponent: SvgComponent;
size?: number;
} }
const { size = 24, SvgComponent } = Astro.props; const {
SvgComponent,
class: className,
role = "presentation",
...rest
} = Astro.props;
--- ---
<SvgComponent <SvgComponent class:list={[className, "inline h-[1em]"]} {role} {...rest} />
height={size}
width={size}
fill="currentColor"
role="presentation"
/>

11
src/duration-utils.ts Normal file
View File

@@ -0,0 +1,11 @@
const UNIX_EPOCH_YEAR = 1970;
export function getDurationInYearsBetween(start: Date, end: Date): number {
return (
new Date(end.getTime() - start.getTime()).getUTCFullYear() - UNIX_EPOCH_YEAR
);
}
export function getDurationInYearsFrom(from: Date): number {
return getDurationInYearsBetween(from, new Date());
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 9-3 3m0 0 3 3m-3-3h7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@@ -0,0 +1,21 @@
---
import "../styles/global.css";
const { title } = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body
class="flex min-h-screen flex-col justify-center p-6 sm:px-14 md:px-24 lg:px-32"
>
<slot />
</body>
</html>

7
src/link-utils.ts Normal file
View File

@@ -0,0 +1,7 @@
export function getRel(
external: boolean | "me" | null | undefined,
): string | undefined {
return external
? "external nofollow noreferrer".concat(external === "me" ? " me" : "")
: undefined;
}

View File

@@ -1,98 +1,134 @@
--- ---
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import ExternalLink from "../components/external-link.astro"; import ButtonLink from "../components/button-link.astro";
import InlineSvg from "../components/inline-svg.astro"; import InlineLink from "../components/inline-link.astro";
import GitHubLogo from "../images/GitHub.svg"; import GitHubLogo from "../images/GitHub.svg";
import GitLabLogo from "../images/GitLab.svg"; import GitLabLogo from "../images/GitLab.svg";
import KeyoxideLogo from "../images/Keyoxide.svg"; import KeyoxideLogo from "../images/Keyoxide.svg";
import LinkedInLogo from "../images/LinkedIn.svg"; import LinkedInLogo from "../images/LinkedIn.svg";
import MatrixLogo from "../images/Matrix-logo.svg"; import MatrixLogo from "../images/Matrix-logo.svg";
import portrait from "../images/portrait.jpg"; import portrait from "../images/portrait.jpg";
import "../styles/global.css"; import BaseLayout from "../layouts/BaseLayout.astro";
--- ---
<!doctype html> <BaseLayout title="kmoschcau.de">
<html lang="de"> <main>
<head> <article class="flex flex-col items-center gap-10">
<meta charset="utf-8" /> <Image
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> alt="Portrait von Kai Moschcau"
<meta name="viewport" content="width=device-width, initial-scale=1" /> class="rounded-full"
<meta name="color-scheme" content="light dark" /> priority
<meta name="generator" content={Astro.generator} /> src={portrait}
<title>kmoschcau.de</title> width={200}
</head> height={200}
<body />
class="flex min-h-screen flex-col justify-center p-6 sm:px-14 md:px-24 lg:px-32" <h1>Kai Moschcau</h1>
> <p class="text-soft">
<main> <strong>PGP fingerprint:</strong>
<article class="flex flex-col items-center gap-10"> <code>0x DF16 F424 1770 90BB</code>
<Image </p>
alt="Portrait von Kai Moschcau" <section class="text-soft">
class="rounded-full" <p><strong>Fachinformatiker</strong> für Anwendungsentwicklung</p>
priority <p><strong>Software Developer &amp; Consultant</strong> @ blecon</p>
src={portrait} </section>
width={200} <nav aria-label="Seitennavigation" class="page-nav">
height={200} <ul class="flex">
/> <li>
<h1>Kai Moschcau</h1> <a href="/skills" class="font-medium underline">Skills</a>
<section class="text-soft"> </li>
<p>Fachinformatiker für Anwendungsentwicklung</p> <li>
</section> <InlineLink
<nav> href="https://blecon.de"
<ul class="flex flex-col gap-2"> external
<li> class="font-medium underline">blecon.de</InlineLink
<ExternalLink >
cssClass="link-matrix" </li>
href="https://matrix.to/#/@kmoschcau:matrix.org" </ul>
title="Mein Matrix-Konto" </nav>
> <nav aria-label="Externe Links">
<InlineSvg SvgComponent={MatrixLogo} slot="logo" /> <ul class="flex flex-col gap-2">
<span>@kmoschcau:matrix.org</span> <li>
</ExternalLink> <ButtonLink
</li> class="link-matrix"
<li> href="https://matrix.to/#/@kmoschcau:matrix.org"
<ExternalLink title="Mein Matrix-Konto"
cssClass="link-github" external="me"
href="https://github.com/kmoschcau/" >
title="Mein GitHub-Konto" <MatrixLogo
> class="size-6"
<InlineSvg SvgComponent={GitHubLogo} slot="logo" /> fill="currentColor"
<span>kmoschcau</span> role="presentation"
</ExternalLink> slot="logo"
</li> />
<li> <span>@kmoschcau:matrix.org</span>
<ExternalLink </ButtonLink>
cssClass="link-gitlab" </li>
href="https://gitlab.com/kmoschcau" <li>
title="Mein GitLab-Konto" <ButtonLink
> class="link-github"
<InlineSvg SvgComponent={GitLabLogo} slot="logo" /> href="https://github.com/kmoschcau/"
<span>kmoschcau</span> title="Mein GitHub-Konto"
</ExternalLink> external="me"
</li> >
<li> <GitHubLogo
<ExternalLink class="size-6"
cssClass="link-linked-in" fill="currentColor"
href="https://www.linkedin.com/in/kmoschcau/" role="presentation"
title="Mein LinkedIn-Konto" slot="logo"
> />
<InlineSvg SvgComponent={LinkedInLogo} slot="logo" /> <span>kmoschcau</span>
<span>kmoschcau</span> </ButtonLink>
</ExternalLink> </li>
</li> <li>
<li> <ButtonLink
<ExternalLink class="link-gitlab"
cssClass="link-keyoxide" href="https://gitlab.com/kmoschcau"
href="https://keyoxide.org/8CE00E9495B5030DA9217208DF16F424177090BB" title="Mein GitLab-Konto"
title="Mein Keyoxide-Eintrag" external="me"
> >
<InlineSvg SvgComponent={KeyoxideLogo} slot="logo" /> <GitLabLogo
<span>Keyoxide</span> class="size-6"
</ExternalLink> fill="currentColor"
</li> role="presentation"
</ul> slot="logo"
</nav> />
</article> <span>kmoschcau</span>
</main> </ButtonLink>
</body> </li>
</html> <li>
<ButtonLink
class="link-linked-in"
href="https://www.linkedin.com/in/kmoschcau/"
title="Mein LinkedIn-Konto"
external="me"
>
<LinkedInLogo
class="size-6"
fill="currentColor"
role="presentation"
slot="logo"
/>
<span>kmoschcau</span>
</ButtonLink>
</li>
<li>
<ButtonLink
class="link-keyoxide"
href="https://keyoxide.org/8CE00E9495B5030DA9217208DF16F424177090BB"
title="Mein Keyoxide-Eintrag"
external="me"
>
<KeyoxideLogo
class="size-6"
fill="currentColor"
role="presentation"
slot="logo"
/>
<span>Keyoxide</span>
</ButtonLink>
</li>
</ul>
</nav>
</article>
</main>
</BaseLayout>

223
src/pages/skills.astro Normal file
View File

@@ -0,0 +1,223 @@
---
import ArrowLeftCircle from "../icons/arrow-left-circle.svg";
import BaseLayout from "../layouts/BaseLayout.astro";
import {
getDurationInYearsBetween as between,
getDurationInYearsFrom as from,
} from "../duration-utils";
import InlineLink from "../components/inline-link.astro";
const trStart = new Date(2011, 7, 1);
const trustStart = new Date(2014, 6, 1);
const migrEnd = new Date(2017, 0, 0);
const cbtStart = new Date(2018, 0, 1);
const pfmStart = new Date(2020, 0, 1);
const cbtEnd = new Date(2023, 7, 0);
const cbt = [cbtStart, cbtEnd] as const;
const mcB2cStart = new Date(2023, 10, 1);
const mcB2cEnd = new Date(2024, 5, 0);
const bpStart = new Date(2024, 5, 1);
const bpEnd = new Date(2025, 1, 0);
const bp = [bpStart, bpEnd] as const;
const kcStart = new Date(2026, 0, 1);
---
<BaseLayout title="kmoschcau.de | Skills">
<main class="flex items-center justify-center">
<article>
<div class="mb-8 flex items-baseline gap-6">
<a
href="/"
aria-label="Zurück"
title="Zurück"
class="text-soft rounded-full"
><ArrowLeftCircle role="presentation" class="size-9" /></a
>
<h1>Skills</h1>
</div>
<div class="prose prose-zinc dark:prose-invert">
<section>
<h2>💻 Softwareentwicklung &amp; Technologien</h2>
<ul>
<li>
Webentwicklung {from(cbtStart)}+ Jahre<br />
(moderne Web-UIs, Responsive Design, komplexe Komponenten, Frontend-Integration,
Web Accessibility, Web App Security)<br />
(HTML, CSS, JavaScript, Typescript, TailwindCSS, daisyUI, SASS, vite,
WebSocket, jQuery, drizzle ORM, Mocha, Chai, Jest)
</li>
<li>
Frontend-Frameworks {from(cbtStart)}+ Jahre<br />
(Erweitern und Umbauen von statischen Web-App Frontends in reactive
Frontends, Neubau von reaktiven Frontends)<br />
(Vue 2, Vue 3, Astro, Svelte, SvelteKit, React)
</li>
<li>
Java {between(...cbt) + from(kcStart)}+ Jahre<br />
(Enterprise B2B und B2C Web-Anwendungen, Web-APIs, Keycloak-Plugins,
saubere Architekturen, Web App Security)<br />
(Spring Boot, Spring MVC, Spring Security, Hibernate, JPA, Freemarker,
Thymeleaf, JSP, REST, JUnit, Mockito, Gradle, Maven)
</li>
<li>
C# - {from(pfmStart)}+ Jahre<br />
(REST- und SOAP-APIs mit hohem Durchsatz, Enterprise-Anwendungen, Web-APIs,
Backend-Systeme, saubere Architekturen, Web App Security)<br />
(.NET 8, .NET Framework 4.8, Entity Framework Core, NHibernate, ASP.NET
Core, Blazor, Razor, REST, SOAP, NUnit, Moq)
</li>
<li>
REST-APIs &amp; Backend-Services - {from(cbtStart)}+ Jahre<br />
(Schnittstellenkonzeption, REST, SignalR, WebSockets)
</li>
</ul>
</section>
<section>
<h2>🛡️ IT-Sicherheit</h2>
<ul>
<li>
Sichere Anwendungsentwicklung &amp; Design {from(cbtStart)}+
Jahre<br />
(OWASP, SSDLC, Threat Modeling, Web App Security)
</li>
<li>
Authentication &amp; Authorization {from(cbtStart)}+ Jahre<br />
(in-App &amp; extern)<br />
(OAuth2, OIDC, Keycloak)
</li>
<li>
Observability im Betrieb {from(cbtStart)}+ Jahre<br />
(Log-Analyse und statistische Auswertung, Telemetrie und Alarme)
</li>
<li>
Sicherheits-Analyse und Tests {between(...cbt)}+ Jahre<br />
(statische und Dynamische Code Analyse, manuelles Penetration Testing)<br
/>
(SonarQube, Veracode, Qualys, Zed Attack Proxy)
</li>
</ul>
</section>
<section>
<h2>☁️ Cloud, DevOps &amp; Betrieb</h2>
<ul>
<li>
Public, Private &amp; Clouds {between(cbtStart, mcB2cEnd)}+
Jahre<br />
(<InlineLink
href="https://www.credly.com/badges/e06dda2d-a444-448d-809a-31565c3b8c8d/public_url"
external="me"
>AWS Certified Solutions Architect Associate</InlineLink
>, AWS, GCP, Azure, CloudFormation, HashiCorp Terraform)
</li>
<li>
CI/CD &amp; DevOps {from(cbtStart)}+ Jahre<br />
(Git, GitHub Actions, GitLab, Gitrunner, Azure DevOps, Jenkins)
</li>
<li>
Containerisierung &amp; Images {from(cbtStart)}+ Jahre<br />
(Docker, Docker Compose, Kubernetes, Helm, HashiCorp Packer)
</li>
<li>
Deployment- &amp; Release-Management {from(cbtStart)}+ Jahre
</li>
</ul>
</section>
<section>
<h2>🗄️ Datenbanken &amp; Persistence</h2>
<ul>
<li>
Relationale Datenbanken {from(trustStart)}+ Jahre<br />
(MySQL, Oracle DB, Microsoft SQL Server, SQLite, PostgreSQL)
</li>
<li>
Datenmodellierung &amp; Performance-Optimierung {
between(...cbt) + between(...bp)
}+ Jahre
</li>
<li>
Data Warehousing {between(trustStart, migrEnd)}+ Jahre
</li>
</ul>
</section>
<section>
<h2>🧱 Architektur &amp; Software Design</h2>
<ul>
<li>
Software-Architektur &amp; Systemdesign {from(cbtStart)}+ Jahre
</li>
<li>
Modulare Systeme &amp; Multitenancy-Architekturen {
from(cbtStart)
}+ Jahre
</li>
<li>
Clean Code, Wartbarkeit &amp; Skalierbarkeit {from(cbtStart)}+
Jahre
</li>
</ul>
</section>
<section>
<h2>🔁 Methoden &amp; Projektarbeit</h2>
<ul>
<li>
Agile Entwicklung (Scrum) {from(trustStart)}+ Jahre
</li>
<li>
Technische Projektverantwortung {
between(...cbt) + between(...bp) + from(kcStart)
}+ Jahre
</li>
<li>
Kundenberatung &amp; technische Abstimmung {from(mcB2cStart)}+
Jahre
</li>
<li>
Code Reviews &amp; technische Qualitätssicherung {
from(cbtStart)
}+ Jahre
</li>
</ul>
</section>
<section>
<h2>🧪 Weitere Technologien &amp; Interessen</h2>
<ul>
<li>
Mentoring &amp; Wissensvermittlung im Team {from(mcB2cStart)}+
Jahre
</li>
<li>
Git &amp; GitHub (Open Source, Versionskontrolle) {
from(trStart)
}+ Jahre
</li>
<li>
Betriebssysteme: Linux (Daily Driver privat), Windows (Daily
Driver Arbeit)
</li>
<li>
Weitere Sprachen, Frameworks und Tools: Ruby, Rust, LaTeX, Lua,
treesitter, Shell, Python, C/C++, SSH, systemd, cron, Vim, Neovim
</li>
<li>
Interesse an 3D-Grafik: 3D Modellierung, Blender, Grundkonzepte
3D-Renderer
</li>
</ul>
</section>
</div>
</article>
</main>
</BaseLayout>

View File

@@ -1,4 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme { @theme {
--color-background-dark: oklch(from var(--color-violet-950) 24% 0.08 h); --color-background-dark: oklch(from var(--color-violet-950) 24% 0.08 h);
@@ -34,7 +35,7 @@
@layer base { @layer base {
body { body {
@apply dark:bg-background-dark bg-neutral-50 text-black dark:text-white; @apply dark:bg-background-dark bg-neutral-50 text-zinc-900 dark:text-zinc-50;
} }
h1 { h1 {
@@ -43,6 +44,10 @@
} }
@layer components { @layer components {
.page-nav li:not(:first-child) {
@apply ms-2 border-s border-zinc-600 ps-2 dark:border-zinc-400;
}
.link-github { .link-github {
@apply bg-github hover:bg-github-hover text-white; @apply bg-github hover:bg-github-hover text-white;
} }