migrate to astro
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
|
@ -1,20 +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: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
||||
9
.gitignore
vendored
|
|
@ -1,8 +1,3 @@
|
|||
.DS_Store
|
||||
.astro
|
||||
dist
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
|
|
|||
1
.npmrc
|
|
@ -1 +0,0 @@
|
|||
engine-strict=true
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
Link.svelte
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
62
README.md
|
|
@ -1,26 +1,64 @@
|
|||
# Alex' small website
|
||||
# daichendt.one
|
||||
|
||||
Link to the [website](https://daichendt.one)
|
||||
Personal website built with Astro, TailwindCSS, and MDX.
|
||||
|
||||
Powered by Svelte & SvelteKit
|
||||
## 🚀 Getting Started
|
||||
|
||||
## Developing
|
||||
### Prerequisites
|
||||
|
||||
Once you've created a project and installed dependencies with `pnpm install`, start a development server:
|
||||
- [Node.js](https://nodejs.org/) (v20 or higher)
|
||||
- [pnpm](https://pnpm.io/) (v8 or higher)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
pnpm dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
pnpm dev -- --open
|
||||
git clone https://github.com/AlexDaichendt/site
|
||||
cd site
|
||||
```
|
||||
|
||||
## Building
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
To create a production version of your app:
|
||||
### Development
|
||||
|
||||
Start the development server:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Building for Production
|
||||
|
||||
Build the project:
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
You can preview the production build with `pnpm run preview`.
|
||||
Preview the production build:
|
||||
```bash
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
## 🛠 Tech Stack
|
||||
|
||||
- [Astro](https://astro.build)
|
||||
- [TailwindCSS](https://tailwindcss.com)
|
||||
- [MDX](https://mdxjs.com)
|
||||
- [Sharp](https://sharp.pixelplumbing.com) for image optimization
|
||||
- [Iconify](https://iconify.design) for icons
|
||||
|
||||
## 📦 Key Dependencies
|
||||
|
||||
- `@astrojs/mdx` - MDX integration
|
||||
- `@astrojs/rss` - RSS feed support
|
||||
- `@astrojs/sitemap` - Sitemap generation
|
||||
- `@astrojs/tailwind` - TailwindCSS integration
|
||||
- `@fontsource/ubuntu` - Ubuntu font
|
||||
- `astro-icon` - Icon component
|
||||
- `remark-emoji` - Emoji support in markdown
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
This theme is based on the [Bear Blog](https://github.com/HermanMartinus/bearblog/) theme.
|
||||
|
|
|
|||
26
astro.config.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from "astro/config";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import remarkEmoji from "remark-emoji";
|
||||
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
|
||||
import icon from "astro-icon";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
prefetch: {
|
||||
defaultStrategy: "hover",
|
||||
prefetchAll: true,
|
||||
},
|
||||
site: "https://daichendt.one",
|
||||
integrations: [
|
||||
mdx({
|
||||
remarkPlugins: [remarkEmoji],
|
||||
}),
|
||||
sitemap(),
|
||||
tailwind(),
|
||||
icon(),
|
||||
],
|
||||
});
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
|
||||
import remarkGFM from 'remark-gfm';
|
||||
import remarkEmoji from 'remark-emoji';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import remarkFootnotes from 'remark-footnotes';
|
||||
|
||||
const config = defineConfig({
|
||||
layout: {
|
||||
blog: './src/lib/layouts/blog.svelte',
|
||||
},
|
||||
extensions: ['.svelte.md', '.md', '.svx'],
|
||||
|
||||
smartypants: {
|
||||
dashes: 'oldschool',
|
||||
},
|
||||
|
||||
remarkPlugins: [remarkGFM, remarkEmoji, remarkFootnotes],
|
||||
rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behaviour: 'append' }]],
|
||||
});
|
||||
|
||||
export default config;
|
||||
82
package.json
|
|
@ -1,58 +1,28 @@
|
|||
{
|
||||
"name": "daichendt.one",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"email": "me@daichendt.one",
|
||||
"name": "Alex Daichendt"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "vite preview",
|
||||
"postinstall": "svelte-kit sync",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
|
||||
"format": "prettier --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-cloudflare": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||
"@typescript-eslint/parser": "^5.47.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"browserslist": "^4.21.4",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"fontaine": "^0.4.1",
|
||||
"mdi-svelte": "^1.1.2",
|
||||
"mdsvex": "^0.10.6",
|
||||
"postcss": "^8.4.20",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"postcss-normalize": "^10.0.1",
|
||||
"prettier": "^2.8.1",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"rehype-autolink-headings": "^6.1.1",
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark-emoji": "^3.0.2",
|
||||
"remark-footnotes": "2.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"svelte": "^3.55.0",
|
||||
"svelte-check": "^3.0.1",
|
||||
"svelte-preprocess": "^5.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.3",
|
||||
"vite-imagetools": "^4.0.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/ubuntu-mono": "^4.5.11",
|
||||
"@mdi/js": "^7.1.96",
|
||||
"imagetools-core": "^3.2.3"
|
||||
},
|
||||
"browserslist": "last 2 versions"
|
||||
"name": "daichendt.one-astro",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.0.1",
|
||||
"@astrojs/rss": "^4.0.10",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/tailwind": "^5.1.3",
|
||||
"@fontsource/ubuntu": "^5.1.0",
|
||||
"@iconify-json/mdi": "^1.2.1",
|
||||
"@iconify-json/simple-icons": "^1.2.14",
|
||||
"astro": "^5.0.3",
|
||||
"astro-icon": "^1.1.4",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwindcss": "^3.4.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwind-plugin/expose-colors": "^1.1.8",
|
||||
"remark-emoji": "^5.0.1"
|
||||
}
|
||||
}
|
||||
7310
pnpm-lock.yaml
generated
9
public/favicon.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
15
src/app.d.ts
vendored
|
|
@ -1,15 +0,0 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface Platform {}
|
||||
// interface Session {}
|
||||
// interface Stuff {
|
||||
// title?: string;
|
||||
// description?: string;
|
||||
// keywords?: string[];
|
||||
// }
|
||||
}
|
||||
16
src/app.html
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-nu-scheme-is="light" data-nu-contrast-is="no-preference">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 551 KiB After Width: | Height: | Size: 551 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
BIN
src/assets/lstopo-matebook.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 410 KiB After Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
61
src/components/BaseHead.astro
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
// Import the global.css file here so that it is included on
|
||||
// all pages through the use of the <BaseHead /> component.
|
||||
import "../styles/global.css";
|
||||
import ubuntuRegularWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-400-normal.woff2?url";
|
||||
import ubuntuBoldWoff2 from "@fontsource/ubuntu/files/ubuntu-latin-700-normal.woff2?url";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
|
||||
const { title, description, image = "/blog-placeholder-1.jpg" } = Astro.props;
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- Font preloads, keep them even if it throws a warning for not using them due to the system providing them -->
|
||||
<link
|
||||
rel="preload"
|
||||
href={ubuntuRegularWoff2}
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href={ubuntuBoldWoff2}
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={new URL(image, Astro.url)} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
||||
148
src/components/DarkModeToggle.astro
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
---
|
||||
|
||||
---
|
||||
|
||||
<script>
|
||||
const themeToggleBtns = document.querySelectorAll(
|
||||
".theme-toggle",
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
const sliders = document.querySelectorAll(".slider");
|
||||
|
||||
// Set initial state of toggle based on previous settings
|
||||
if (
|
||||
localStorage.getItem("color-theme") === "dark" ||
|
||||
(!("color-theme" in localStorage) &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
themeToggleBtns.forEach((btn) => (btn.checked = true));
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
themeToggleBtns.forEach((btn) => (btn.checked = false));
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
// Remove no-transition class after initial load
|
||||
window.addEventListener("load", () => {
|
||||
setTimeout(() => {
|
||||
sliders.forEach((slider) =>
|
||||
slider.classList.remove("no-transition"),
|
||||
);
|
||||
}, 0);
|
||||
});
|
||||
themeToggleBtns.forEach((btn) => {
|
||||
btn.addEventListener("change", function () {
|
||||
// If is set in localStorage
|
||||
if (localStorage.getItem("color-theme")) {
|
||||
if (localStorage.getItem("color-theme") === "light") {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.setItem("color-theme", "dark");
|
||||
themeToggleBtns.forEach((btn) => (btn.checked = true));
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.setItem("color-theme", "light");
|
||||
themeToggleBtns.forEach((btn) => (btn.checked = false));
|
||||
}
|
||||
} else {
|
||||
if (document.documentElement.classList.contains("dark")) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.setItem("color-theme", "light");
|
||||
themeToggleBtns.forEach((btn) => (btn.checked = false));
|
||||
} else {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.setItem("color-theme", "dark");
|
||||
themeToggleBtns.forEach((btn) => (btn.checked = true));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<label class="switch" for="theme-toggle">
|
||||
<span class="sr-only">Toggle dark mode</span>
|
||||
<input
|
||||
id="theme-toggle"
|
||||
class="theme-toggle"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
aria-checked="false"
|
||||
/>
|
||||
<span class="slider round no-transition" aria-hidden="true"></span>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--tw-mytheme-200);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: gold;
|
||||
transition: 0.4s;
|
||||
background: radial-gradient(
|
||||
yellow,
|
||||
orange 63%,
|
||||
transparent calc(63% + 3px) 100%
|
||||
);
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--tw-mytheme-600);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
background-color: white;
|
||||
background: radial-gradient(
|
||||
circle at 19% 19%,
|
||||
transparent 41%,
|
||||
var(--tw-mytheme-50) 43%
|
||||
);
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 5px var(--tw-mytheme-700);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.no-transition {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.no-transition:before {
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
14
src/components/Footer.astro
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
const today = new Date();
|
||||
---
|
||||
|
||||
<footer class="bg-gray-100/60 dark:bg-mytheme-900 shadow-sm">
|
||||
© {today.getFullYear()} Alexander Daichendt. All rights reserved.
|
||||
</footer>
|
||||
<style>
|
||||
footer {
|
||||
padding: 2em 1em 6em 1em;
|
||||
color: rgb(var(--gray));
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
17
src/components/FormattedDate.astro
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
interface Props {
|
||||
date: Date;
|
||||
}
|
||||
|
||||
const { date } = Astro.props;
|
||||
---
|
||||
|
||||
<time datetime={date.toISOString()}>
|
||||
{
|
||||
date.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
</time>
|
||||
14
src/components/Header.astro
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import HeaderLink from "./HeaderLink.astro";
|
||||
---
|
||||
|
||||
<header class="mb-8 w-full lg:w-[768px] max-w-[calc(100%-2em)] lg:mx-auto">
|
||||
<nav>
|
||||
<div class="flex gap-4">
|
||||
<HeaderLink href="/">Home</HeaderLink>
|
||||
<HeaderLink href="/blog">Blog</HeaderLink>
|
||||
<HeaderLink href="/publications">Publications</HeaderLink>
|
||||
<HeaderLink href="/contact">Contact</HeaderLink>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
56
src/components/HeaderLink.astro
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
import type { HTMLAttributes } from "astro/types";
|
||||
|
||||
type Props = HTMLAttributes<"a">;
|
||||
|
||||
const { href, class: className, ...props } = Astro.props;
|
||||
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, "");
|
||||
const subpath = pathname.match(/[^\/]+/g);
|
||||
const isActive = href === pathname || href === "/" + (subpath?.[0] || "");
|
||||
---
|
||||
|
||||
<a
|
||||
href={href}
|
||||
class:list={[
|
||||
className,
|
||||
isActive ? "active" : "",
|
||||
"p-2 hover:text-mytheme-800 hover:dark:text-mytheme-100 inline-block no-underline relative",
|
||||
]}
|
||||
{...props}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
|
||||
<style>
|
||||
/* Hover animation for non-active links */
|
||||
a:not(.active)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 4px;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
background-color: var(--tw-mytheme-400);
|
||||
transition: all 0.3s ease-in-out;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
a:not(.active):hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Solid underline for active links */
|
||||
a.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--tw-mytheme-400);
|
||||
}
|
||||
|
||||
a.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
5
src/components/Li.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<li class="pt-2 mr-4" {...Astro.props}>
|
||||
<div class="self-center">
|
||||
<slot />
|
||||
</div>
|
||||
</li>
|
||||
27
src/components/Link.astro
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
interface Props {
|
||||
href: string;
|
||||
disablePrefetch?: boolean;
|
||||
}
|
||||
|
||||
const { href, disablePrefetch = false } = Astro.props;
|
||||
|
||||
const internal = !href.startsWith("http");
|
||||
|
||||
let linkProps = internal
|
||||
? !disablePrefetch
|
||||
? { "data-astro-prefetch": `${!disablePrefetch}` }
|
||||
: {}
|
||||
: {
|
||||
rel: "nofollow noreferrer noopener",
|
||||
target: "_blank",
|
||||
};
|
||||
---
|
||||
|
||||
<a
|
||||
{...linkProps}
|
||||
{href}
|
||||
class="text-special text-mytheme-700 dark:text-mytheme-300 font-medium hover:bg-outline hover:text-dark no-underline"
|
||||
>
|
||||
<span class="underline break-words"><slot /></span></a
|
||||
>
|
||||
3
src/components/Ol.astro
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<ol class="list-disc list-inside" {...Astro.props}>
|
||||
<slot />
|
||||
</ol>
|
||||
9
src/components/Picture.astro
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import { Picture as AstroPicture } from "astro:assets";
|
||||
---
|
||||
|
||||
<AstroPicture
|
||||
src={Astro.props.src}
|
||||
alt={Astro.props.alt}
|
||||
formats={["avif", "webp"]}
|
||||
/>
|
||||
3
src/components/Ul.astro
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<ul class="list-disc list-outside ml-8" {...Astro.props}>
|
||||
<slot />
|
||||
</ul>
|
||||
6
src/consts.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Place any global data in this file.
|
||||
// You can import this data from anywhere in your site by using the `import` keyword.
|
||||
|
||||
export const SITE_TITLE = "Alex Daichendt";
|
||||
export const SITE_DESCRIPTION =
|
||||
"Alex Daichendt's personal website, blog, and portfolio.";
|
||||
19
src/content.config.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { glob } from "astro/loaders";
|
||||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
const blog = defineCollection({
|
||||
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
||||
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
|
||||
// Type-check frontmatter using a schema
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
// Transform string to Date object
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
heroImage: image().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2023-01-02'
|
||||
pubDate: '2023-01-02'
|
||||
title: 'Generate cover letters with ChatGPT'
|
||||
description: 'With the help of ChatGPT it is fairly easy to generate custom tailored cover letters for job applications with your own CV'
|
||||
keywords:
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
---
|
||||
created: '2024-01-15'
|
||||
title: 'Detecting System Management Interrupts (SMIs)'
|
||||
pubDate: '2024-01-15'
|
||||
title: 'Detecting System Management Interrupts'
|
||||
description: ''
|
||||
keywords:
|
||||
- SMI
|
||||
---
|
||||
|
||||
# System Management Interrutps (SMI)
|
||||
## System Management Interrutps (SMIs)
|
||||
|
||||
- high priority interrupts caused by the hardware
|
||||
- transparent to the operating system
|
||||
- can be used by the mainboard for power management, thermal management, or other system-level functions independent of the OS
|
||||
- can take a long time to execute, causing a CPU core to be blocked from other work
|
||||
|
||||
## Detecting SMIs
|
||||
### Detecting SMIs
|
||||
|
||||
- compile a kernel with hwlat tracing capabilities; usually, a typical Linux kernel has this enabled; if not, the config can be found in the appendix
|
||||
- after starting the machine with a trace-capable image
|
||||
|
|
@ -22,7 +22,7 @@ keywords:
|
|||
- there now should be a process "hwlat" running that takes up 50% of one CPU
|
||||
- output of the hwlat tracer available `cat /sys/kernel/debug/tracing/trace` or `cat /sys/kernel/debug/tracing/trace_pipep`
|
||||
|
||||
## Example Output
|
||||
### Example Output
|
||||
|
||||
```
|
||||
# tracer: hwlat
|
||||
|
|
@ -41,7 +41,7 @@ keywords:
|
|||
|
||||
- inner/outer: where the latency was detected, see next section
|
||||
|
||||
### How does it work?
|
||||
#### How does it work?
|
||||
|
||||
- this hwlat process is taking timestamps in a loop
|
||||
- if distance between two timestamps is unreasonably large (bigger than ns), there was an SMI
|
||||
|
|
@ -62,18 +62,18 @@ keywords:
|
|||
}
|
||||
```
|
||||
|
||||
### Further options
|
||||
#### Further options
|
||||
|
||||
- by default, only 50% CPU time is used
|
||||
- this can be increased by echoing into `echo 9999999 > /sys/kernel/debug/tracing/hwlat_detector/width`, where the value is smaller than the set window `cat /sys/kernel/debug/tracing/hwlat_detector/window` to avoid starving the system.
|
||||
- from my experience, this, however, is not necessary to catch SMIs. The default option is "good enough".
|
||||
|
||||
## Firing an SMI manually
|
||||
### Firing an SMI manually
|
||||
|
||||
- There is a nice small kernel module [here](https://github.com/jib218/kernel-module-smi-trigger) for manually triggering an SMI to verify the setup
|
||||
- follow the instructions in the readme to compile and load the module
|
||||
|
||||
## Hardware Registers for counting SMIs
|
||||
### Hardware Registers for counting SMIs
|
||||
|
||||
- Intel: MSR0x34, can be read out with turbostat / perf
|
||||
- AMD: ls\_msi\_rx, can be used with `perf stat -e ls_smi_rx -I 60000`
|
||||
|
|
@ -81,7 +81,7 @@ However, doesn't seem to count everything; counts seem incorrect
|
|||
|
||||
---
|
||||
|
||||
## Sources, Appendix
|
||||
### Sources, Appendix
|
||||
|
||||
- https://wiki.linuxfoundation.org/realtime/documentation/howto/tools/hwlat
|
||||
- https://www.kernel.org/doc/html/latest/trace/hwlat_detector.html
|
||||
66
src/content/blog/huawei-matebook-x-pro-2024.mdx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
pubDate: 2024-12-21
|
||||
title: Linux on a Huawei MateBook X Pro 2024
|
||||
description: A guide on what is needed to get Linux running on the Huawei MateBook X Pro 2024.
|
||||
heroImage: ./images/matebook.jpg
|
||||
---
|
||||
|
||||
I recently bought a Huawei MateBook X Pro 2024. It is a beautiful laptop with a 3:2 aspect ratio display and a touchscreen. The laptop comes with Windows 11 preinstalled. However, I wanted to run Linux on it. Here is a guide on what is needed to get Linux running on the Huawei MateBook X Pro 2024.
|
||||
|
||||
Overall, the experience was okay, but not something I would recommend to an average user. There are a fair bit of quirks that need to be ironed out. Especially distros running older kernels will have a hard time. I am running CachyOS with the latest 6.13-rc1 kernel, more on that later.
|
||||
|
||||
| Hardware | PCI/USB ID | Status |
|
||||
| ----------- | ------------------------------------------- | ------------------ |
|
||||
| CPU | | :white_check_mark: |
|
||||
| Touchpad | ps/2:7853-7853-bltp7840-00-347d | :white_check_mark: |
|
||||
| Touchscreen | | :white_check_mark: |
|
||||
| Keyboard | ps/2:0001-0001-at-translated-set-2-keyboard | :white_check_mark: |
|
||||
| WiFi | 8086:7e40 | :white_check_mark: |
|
||||
| Bluetooth | 8087:0033 | :white_check_mark: |
|
||||
| iGPU | 8086:7d55 | :neutral_face: |
|
||||
| Audio | 8086:7e28 | :ok: |
|
||||
| Webcam | 8086:7d19 | :x: |
|
||||
| Fingerprint | | :x: |
|
||||
|
||||
## CPU
|
||||
|
||||
The CPU on my SKU is an Intel Meteor Lake Core Ultra 155H. It comes with 6 performance cores, each with 2 threads, 8 efficiency cores, one thread each, and 2 LPE cores. The p and e cores share 24MB of L3 cache. The LPE cores do not have L3 cache and share 2MB L2 cache, which makes them rather slow. Below you can find the output of `lstopo`:
|
||||
|
||||

|
||||
|
||||
Since thread director is not yet supported in the Linux kernel, by default, the scheduler will assign processes to the performance cores--while on battery. A scheduler like bpfland helps, but that still leaves the first, CPU core 0, alive. Disabling the cores manually is also not a good solution as the core 0 can not be deactivated. There used to be a kernel config option, `CONFIG_BOOTPARAM_HOTPLUG_CPU0` which would allow the core to be disabled at runtime, but is no longer available[^1].
|
||||
|
||||
Luckily, Intel is developing a tool which utilizes cgroups to enable/disable cores at runtime and moves processes away. If you care about battery life, you might want to configure `intel-lpmd`[^2].
|
||||
After installing the tool, it must be enabled with `sudo systemctl enable --now intel-lpmd`. Next, enter your p cores into the config file at `/etc/intel_lpmd/intel_lpmd_config.xml`, so if you are running with SMT enabled, it would be the string `0-11` to include the 6 p-cores with 2 threads each. When you are on battery, the tool will disable the p-cores and move processes away. You can verify that it is active with `sudo systemctl status intel_lpmd.service`. For additional battery-savings, you can also disable the e-cores as the L3 cache can then be powered down. I would not recommend it tho.
|
||||
|
||||
## Touchpad
|
||||
|
||||
The touchpad worked out of the box ever since I got the laptop. I did read that older kernels might not register it.
|
||||
|
||||
## Touchscreen, Keyboard, Wifi, Bluetooth
|
||||
|
||||
No problems, as far as I can tell, all work out of the box.
|
||||
|
||||
## iGPU
|
||||
|
||||
This is a big one. Theres a problem with the default i915 driver which causes the iGPU to never go into a low power state. This is a big problem as it drains the battery rather quickly. There is an experimental Intel Xe driver, which fixes this issue. It can be enabled by adding the kernel parameters `i915.force_probe=!7d55 xe.force_probe=7d55` to the kernel command line. The driver is already in mainline, so no need to compile it yourself. However, the driver is still experimental there are several bugs. The screen might flicker from time to time showing rectangular artifacts. The 6.12 or lower Xe driver was highly unstable and caused my system to hard lock every few minutes. The 6.13-rc1 driver is much more stable, asides from the artifacts.
|
||||
|
||||
## Audio
|
||||
|
||||
The audio works out of the box. But its not great. It seems like not all speakers are fully used. It is good enough for me tho.
|
||||
|
||||
## Webcam
|
||||
|
||||
The webcam is an ipu6-based camera. Support has been trickling in over the years, but it is unusable at the moment and the forseeable future.
|
||||
|
||||
## Fingerprint
|
||||
|
||||
The fingerprint sensor is not supported at the moment. It does not even show up anywhere. One of those ingenious Goodix sensors that are not supported by the fprintd library.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Sources:
|
||||
|
||||
[^1]: https://www.kernelconfig.io/search?q=CONFIG_BOOTPARAM_HOTPLUG_CPU0&kernelversion=6.12.4&arch=x86
|
||||
[^2]: https://github.com/intel/intel-lpmd
|
||||
49
src/content/blog/images/kagi_doggo_5.svg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
src/content/blog/images/matebook.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
src/content/blog/images/msi.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
|
|
@ -1,15 +1,12 @@
|
|||
---
|
||||
created: '2024-06-11'
|
||||
pubDate: '2024-06-11'
|
||||
title: "Kagi.com"
|
||||
description: ""
|
||||
description: "Thoughts on Kagi.com"
|
||||
keywords:
|
||||
- search engine
|
||||
hidden: false
|
||||
heroImage: ./images/kagi_doggo_5.svg
|
||||
---
|
||||
<script>
|
||||
import fastgpt from "./images/fastgpt.png?default"
|
||||
import Image from "$components/Image.svelte"
|
||||
</script>
|
||||
|
||||
Kagi is a paid search engine providing excellent search that reminds me of what Google was like in the early 2000s. Furthermore, it provides search-enhancing features like specific filters, custom site rankings, and an LLM summary of the search results.
|
||||
In this post, I would like to share my thoughts on Kagi.com and explain why I think it is a great search engine despite recent criticism.
|
||||
|
|
@ -23,7 +20,7 @@ If, for some reason, a bad site appears in the search results, I can easily bloc
|
|||
## AI Summary
|
||||
Kagi's AI will summarize the search results by simply appending a `?` to the end of the search query. LLMs are prone to generating nonsense, but Kagi's AI adds citations with links to the original source. If the AI summary provided helpful information, it was accurate; if it did not, the results were still there.
|
||||
|
||||
<Image meta={fastgpt} alt="Example search query"/>
|
||||

|
||||
|
||||
## Privacy
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2024-08-25'
|
||||
pubDate: '2024-08-25'
|
||||
title: "Kata Containers: Custom Kernel Module in Guest"
|
||||
description: 'How to build a custom kernel module for a Kata Containers guest.'
|
||||
keywords:
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2022-09-24'
|
||||
pubDate: '2022-09-24'
|
||||
title: 'Securing a Caddy endpoint with LLDAP'
|
||||
description: ''
|
||||
keywords:
|
||||
|
|
@ -7,11 +7,6 @@ keywords:
|
|||
- Caddy
|
||||
---
|
||||
|
||||
<script>
|
||||
import overview from "./lldap_overview.png?default"
|
||||
import Image from "$components/Image.svelte"
|
||||
</script>
|
||||
|
||||
For my small home network, I was looking around for a solution to synchronize user
|
||||
accounts across services. I host various services like a file server or smaller web
|
||||
applications that are accessed by my significant other and a couple of friends. In the
|
||||
|
|
@ -46,7 +41,7 @@ bridge for networking so that I can resolve my other services with DNS. After th
|
|||
navigate to http://IP:17170 and are presented with the administration panel, where we can
|
||||
create users and groups.
|
||||
|
||||
<Image meta={overview} alt="LLDAP Userinterface"/>
|
||||

|
||||
|
||||
## Integration with Caddy
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2022-07-27'
|
||||
pubDate: '2022-07-27'
|
||||
title: 'How to: Run a DPDK application in an LXC container'
|
||||
description: ''
|
||||
keywords:
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2023-03-12'
|
||||
pubDate: '2023-03-12'
|
||||
title: '[Workaround] High idle power consumption with AMD GPUs'
|
||||
description: There is a bug with AMD gpus on Linux that causes the memory to run at full speed even when the GPU is idle for certain refresh rates and resolutions. This causes high idle power consumption. This post explains how to fix it.
|
||||
keywords:
|
||||
|
|
@ -17,19 +17,13 @@ keywords:
|
|||
- linux kernel
|
||||
- kernel
|
||||
hidden: false
|
||||
updated: '2023-08-02'
|
||||
updatedDate: '2023-08-02'
|
||||
---
|
||||
|
||||
<script>
|
||||
import hz120 from "./images/_120hz.png?default"
|
||||
import hz144low from "./images/_144hzlow.png?default"
|
||||
|
||||
import Image from "$components/Image.svelte"
|
||||
</script>
|
||||
|
||||
For many years, AMD GPUs have had a bug that causes the memory to run at full speed even when the GPU is idle for certain refresh rates and resolutions. For example, I am running a 34' UWQHD 3440x1440 monitor with 144Hz refresh rate. My GPU is a 6700 XT with 16GB of VRAM. When I set the resolution to 3440x1440 and the refresh rate to 144Hz, the memory frequency is stuck at 1000 MHz, or as Windows would report it 2000 Mhz. The high memory frequency consumes about 30 W of power - not doing anything at all.
|
||||
|
||||
<Image meta={hz120} alt="High power draw and high mem frequency"/>
|
||||

|
||||
|
||||
This bug apparently existed on Windows as well, but was fixed some time in 2021. There were some tries to fix the issue on Linux in the past, but obviously nothing worked - as I found out now running the latest kernel 6.2.
|
||||
|
||||
|
|
@ -57,7 +51,7 @@ The power draw is now as expected at around 8W. The memory downclocks to 96Mhz.
|
|||
|
||||
This might not seem like a lot, but in situations like this I have to think about how many GPUs were sold globally and are affected by this bug. It's is not an insignificant amount of power that is wasted on a global scale.
|
||||
|
||||
<Image meta={hz144low} alt="High power draw and high mem frequency"/>
|
||||

|
||||
|
||||
_Update: 2021-08-02_:
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2023-02-04'
|
||||
pubDate: '2023-02-04'
|
||||
title: You don't need an AIO
|
||||
description: AIOs are overrated tech that's recommended too frequently without considering the downsides
|
||||
keywords:
|
||||
|
|
@ -9,7 +9,7 @@ keywords:
|
|||
- CPU cooler
|
||||
---
|
||||
|
||||
Recently, I have been browsing hardware forums to get in touch with the current state of computer hardware again. I am in the process of upgrading my 6-year-old rig. Up until yesterday, I had a Corsair AIO (All in one) CPU cooler with a 280mm radiator cooling my heavily overclocked CPU. Unfortunately, this AIO is not supported by Liquidctl, and the other legacy software for controlling it no longer works. So I was stuck with a piece of working hardware that I could no longer configure on my operating system of choice. I ended up buying a nice Noctua CPU cooler and ever since my PC is more enjoyable to use in every aspect. Let's get into the details, of why I think AIOs are (for most people) a bad pick.
|
||||
Recently, I have been browsing hardware forums to get in touch with the current state of computer hardware again. I am in the process of upgrading my 6-year-old rig. Up until yesterday, I had a Corsair AIO (All in one) CPU cooler with a 280mm radiator cooling my heavily overclocked CPU. Unfortunately, this AIO is not supported by Liquidctl, the software to control AIOs on Linux, and the other legacy software for controlling it no longer works. So I was stuck with a piece of working hardware that I could no longer configure on my operating system of choice. I ended up buying a nice Noctua CPU cooler and ever since my PC is more enjoyable to use in every aspect. Let's get into the details, of why I think AIOs are (for most people) a bad pick.
|
||||
|
||||
(this is not a Noctua-sponsored post)
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2022-08-04'
|
||||
pubDate: '2022-08-04'
|
||||
title: 'Free, on-demand image optimizations with Cloudflare'
|
||||
description: ''
|
||||
keywords:
|
||||
|
|
@ -10,12 +10,8 @@ keywords:
|
|||
hidden: true
|
||||
---
|
||||
|
||||
<script>
|
||||
import architecture from "./_architecture.png?default"
|
||||
import Image from "$components/Image.svelte"
|
||||
</script>
|
||||
|
||||
I wanted to use responsive images for my small page of cute [cats](/cat). Since
|
||||
I wanted to use responsive images for my small page of cute [cats](/cat) (now removed as of 8/12/2024). Since
|
||||
one of the design goals is to give my significant other, who by the way loves
|
||||
cats a lot more than me, the option to add cats on the fly and also consume the
|
||||
cat pictures in other services, I require dynamic image optimization.
|
||||
|
|
@ -43,7 +39,7 @@ optimization program on a Raspberry Pi at my place. It is connected with a
|
|||
Cloudflare tunnel to the internet. Any other device that has computing power and
|
||||
is accessible via the internet is alright.
|
||||
|
||||
<Image meta={architecture} />
|
||||

|
||||
|
||||
- **(1)**: a user uploads a new image, for example via this site
|
||||
- **(2)**: the worker processes forwards the image to the image optimization server
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2023-06-19'
|
||||
pubDate: '2023-06-19'
|
||||
title: 'Optimzing the Guild Wars 2 Gear Optimizer'
|
||||
description: ''
|
||||
keywords:
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2022-05-08'
|
||||
pubDate: '2022-05-08'
|
||||
title: 'How to: Arrow OS on Redmi Note 7, root, microG'
|
||||
description: 'Learn how to install ArrowOS, based on Android 12 on your Redmi Note 7 (lavender) phone! Also installs root and microG for a BigTech free phone.'
|
||||
keywords:
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
created: '2022-09-27'
|
||||
updated: '2022-10-22'
|
||||
pubDate: '2022-09-27'
|
||||
updatedDate: '2022-10-22'
|
||||
title: 'Site 2 Site Wireguard VPN with a Mikrotik Router and a Cloud VM'
|
||||
description: ''
|
||||
keywords:
|
||||
|
|
@ -11,13 +11,6 @@ keywords:
|
|||
- vpn
|
||||
---
|
||||
|
||||
<script>
|
||||
import architecture from "./images/_architecture.drawio.png?default"
|
||||
import peer from "./images/mikrotik_peer.png?default"
|
||||
|
||||
import Image from "$components/Image.svelte"
|
||||
</script>
|
||||
|
||||
My network consists of a server located in country A. Since the largest ISP in the country
|
||||
B does have terrible peering with the ISP in country A, I thought of setting up a small
|
||||
proxy server in country A. This way, I should be able to bypass bad peering, since the
|
||||
|
|
@ -25,7 +18,7 @@ cloud provider probably organizes good routing to both sides. Since I meant to t
|
|||
Oracles free tier anyway, it seemed like a good opportunity to learn ansible properly and
|
||||
develop with IaC scripts to set up a reverse proxy in the cloud.
|
||||
|
||||
<Image meta={architecture} alt="Architecture of the site two site wireguard setup"/>
|
||||

|
||||
|
||||
1. Create Wireguard keys. If the CLI is not an option [this
|
||||
website](https://www.wireguardconfig.com/) is cool too (keys are client-sided generated)
|
||||
|
|
@ -35,7 +28,7 @@ develop with IaC scripts to set up a reverse proxy in the cloud.
|
|||
3. Create a new peer as follows. Important is the entry to allow the IP address of the
|
||||
cloud wg endpoint, otherwise the cloud cant ping back home.
|
||||
<div style="max-width:600px">
|
||||
<Image meta={peer} alt="Mikrotik router peer setup"/>
|
||||

|
||||
</div>
|
||||
4. I had to adjust the firewall rules to allow communication with the tunnel network.
|
||||
5. On the proxy server we use similar settings. Interestingly enough, the Mikrotik wg
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
created: '2022-07-26'
|
||||
updated: '2022-07-27'
|
||||
pubDate: '2022-07-26'
|
||||
updatedDate: '2022-07-27'
|
||||
title: 'Software recommendations for privacy conscious people'
|
||||
description: ''
|
||||
keywords:
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2022-09-29'
|
||||
pubDate: '2022-09-29'
|
||||
title: "My Bachelor's thesis journey at TUM"
|
||||
description: ''
|
||||
keywords:
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
created: '2024-08-10'
|
||||
title: "Experience with MSI warranty for a monitor"
|
||||
pubDate: '2024-08-10'
|
||||
updatedDate: '2024-12-08'
|
||||
title: "Experience with MSI's warranty for a monitor"
|
||||
description: "Positive review of MSI's warranty service for a monitor."
|
||||
keywords:
|
||||
- warranty
|
||||
|
|
@ -10,6 +11,7 @@ keywords:
|
|||
- bug
|
||||
- thunderfly
|
||||
hidden: false
|
||||
heroImage: ./images/msi.png
|
||||
---
|
||||
|
||||
I bought a new 32' 4k MSI Monitor for my birthday a couple of months ago. Since there was not much choice when it came to my requirements:
|
||||
|
|
@ -20,7 +22,7 @@ I bought a new 32' 4k MSI Monitor for my birthday a couple of months ago. Since
|
|||
- 4k, 32',
|
||||
I ended up buying the MSI MAG 323UPF, albeit its 'gamer' design. The device and its productivity features are awesome, and I'm very happy with them.
|
||||
|
||||
One day, a couple of dead pixels appeared. I was not sure if they were dead pixels or bugs, but they were not moving. Since they were about 8 pixels in a perfect line, I assumed it was a bug, but I do not know for sure. I was devastated.
|
||||
One day, a couple of dead pixels appeared. I was not sure if they were dead pixels or bugs, but they were not moving. Since they were about 8 pixels in a perfect line, I assumed it was a bug, but I do not know for sure. Searching the web it seems common occurence. I was devastated.
|
||||
|
||||
None of the tricks online worked:
|
||||
- setup another light source to lure it outside
|
||||
|
|
@ -30,7 +32,7 @@ None of the tricks online worked:
|
|||
|
||||
Maybe it was a juicy one and died in the middle of the screen.
|
||||
|
||||
I contacted MSI support, and they were very helpful. After uploading a picture, it did not take 5 minutes to receive a return label. A day later, the UPS guy picked it up at my door. The package was then delivered to Poland (I live in Germany). Once it arrived there, it only took a couple of hours until I received an email claiming that the monitor was defective, and I got a free replacement immediately.
|
||||
I contacted MSI support, and they were very helpful. After uploading a picture, it did not take 5 minutes to receive a return label. A day later, the UPS guy picked it up at my door. The package was then delivered to Poland (I live in Germany). Once it arrived there, it only took a couple of hours until I received an email claiming that the monitor was defective, and I got a free replacement immediately. Granted, I bribed the technician with a pack of Haribo gummy bears, but I'm sure that was not the reason for the quick replacement. A little kindness goes a long way.
|
||||
|
||||
Kudos to MSI for their excellent support. Whether it was a bug or a dead pixel, neither should happen on a new monitor. I'm glad they stick to their warranty without any hassle.
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
created: '2022-08-12'
|
||||
pubDate: '2022-08-12'
|
||||
title: "Do's and don't when writing a thesis"
|
||||
description: 'Useful tips for avoiding common mistakes when writing a thesis. Includes recommendations for writing, formatting, figures and Latex.'
|
||||
keywords:
|
||||
77
src/data/publications.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
export const publications = [
|
||||
{
|
||||
authors: [
|
||||
"Florian Wiedner",
|
||||
"Max Helm",
|
||||
"Alexander Daichendt",
|
||||
"Jonas Andre",
|
||||
"Georg Carle",
|
||||
],
|
||||
title:
|
||||
"Performance evaluation of containers for low-latency packet processing in virtualized network environments",
|
||||
journal: "Performance Evaluation",
|
||||
volume: "166",
|
||||
date: "Nov. 2024",
|
||||
pages: "102442",
|
||||
links: {
|
||||
doi: "https://doi.org/10.1016/j.peva.2024.102442",
|
||||
pdf: "http://www.net.in.tum.de/fileadmin/bibtex/publications/papers/wiedner-helm-2024-peva.pdf",
|
||||
homepage: "https://wiednerf.github.io/container-in-low-latency/",
|
||||
bibtex: "/publications/bibtex/WiedHelm24Container.bib",
|
||||
},
|
||||
},
|
||||
{
|
||||
authors: [
|
||||
"Alexander Daichendt",
|
||||
"Florian Wiedner",
|
||||
"Jonas Andre",
|
||||
"Georg Carle",
|
||||
],
|
||||
title:
|
||||
"Applicability of Hardware-Supported Containers in Low-Latency Networking",
|
||||
conference:
|
||||
"20th International Conference on Network and Service Management (CNSM 2024)",
|
||||
location: "Prague, Czech Republic",
|
||||
date: "Oct. 2024",
|
||||
links: {
|
||||
pdf: "http://www.net.in.tum.de/fileadmin/bibtex/publications/papers/wiedner_2024_cnsm.pdf",
|
||||
homepage: "https://tumi8.github.io/applicability-hwsupported-containers",
|
||||
bibtex: "https://www.net.in.tum.de/publications/bibtex/Wied24CNSM.bib",
|
||||
},
|
||||
},
|
||||
{
|
||||
authors: [
|
||||
"Florian Wiedner",
|
||||
"Alexander Daichendt",
|
||||
"Jonas Andre",
|
||||
"Georg Carle",
|
||||
],
|
||||
title: "Control Groups Added Latency in NFVs: An Update Needed?",
|
||||
conference:
|
||||
"2023 IEEE Conference on Network Function Virtualization and Software Defined Networks (NFV-SDN)",
|
||||
date: "Nov. 2023",
|
||||
links: {
|
||||
pdf: "https://www.net.in.tum.de/fileadmin/bibtex/publications/papers/wiedner_nfvsdn2023.pdf",
|
||||
homepage: "https://wiednerf.github.io/cgroups-nfv/",
|
||||
bibtex: "/publications/bibtex/wiedner2023containercgroups.bib",
|
||||
},
|
||||
},
|
||||
{
|
||||
authors: [
|
||||
"Florian Wiedner",
|
||||
"Max Helm",
|
||||
"Alexander Daichendt",
|
||||
"Jonas Andre",
|
||||
"Georg Carle",
|
||||
],
|
||||
title:
|
||||
"Containing Low Tail-Latencies in Packet Processing Using Lightweight Virtualization",
|
||||
conference: "2023 35rd International Teletraffic Congress (ITC-35)",
|
||||
date: "Oct. 2023",
|
||||
links: {
|
||||
pdf: "https://www.net.in.tum.de/fileadmin/bibtex/publications/papers/wiedner_itc35.pdf",
|
||||
homepage: "https://wiednerf.github.io/containerized-low-latency/",
|
||||
bibtex: "/publications/bibtex/wiedner2023container.bib",
|
||||
},
|
||||
},
|
||||
];
|
||||
75
src/layouts/BaseLayout.astro
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
import BaseHead from "../components/BaseHead.astro";
|
||||
import Header from "../components/Header.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
|
||||
import DarkModeToggle from "../components/DarkModeToggle.astro";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<BaseHead title={title} description={description} />
|
||||
<script is:inline>
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
if (
|
||||
localStorage.getItem("color-theme") === "dark" ||
|
||||
(!("color-theme" in localStorage) &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900 text-black dark:text-white">
|
||||
<!-- Mobile layout -->
|
||||
<div class="lg:hidden flex flex-col min-h-screen p-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="font-bold text-lg">
|
||||
<a href="/">{SITE_TITLE}</a>
|
||||
</h2>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
|
||||
<Header />
|
||||
|
||||
<main class="flex-grow">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Desktop layout -->
|
||||
<div
|
||||
class="hidden lg:grid grid-cols-[150px_1fr_60px] gap-4 min-h-screen p-4"
|
||||
>
|
||||
<div class="flex items-start">
|
||||
<h2 class="font-bold text-xl mt-3">
|
||||
<a href="/">{SITE_TITLE}</a>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Header />
|
||||
<main
|
||||
class="w-full lg:w-[768px] max-w-[calc(100%-2em)] mx-auto p-2"
|
||||
>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
78
src/layouts/BlogPost.astro
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import FormattedDate from "../components/FormattedDate.astro";
|
||||
import BaseLayout from "./BaseLayout.astro";
|
||||
import { Picture } from "astro:assets";
|
||||
|
||||
type Props = CollectionEntry<"blog">["data"];
|
||||
|
||||
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description}>
|
||||
<article>
|
||||
<div class="hero-image">
|
||||
{
|
||||
heroImage && (
|
||||
<Picture src={heroImage} alt="" width={1020} height={510} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div class="prose">
|
||||
<div class="title">
|
||||
<div class="date">
|
||||
<FormattedDate date={pubDate} />
|
||||
{
|
||||
updatedDate && (
|
||||
<div class="last-updated-on">
|
||||
Last updated on{" "}
|
||||
<FormattedDate date={updatedDate} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<h1>{title}</h1>
|
||||
<hr />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
main {
|
||||
width: calc(100% - 2em);
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
}
|
||||
.hero-image img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
.prose {
|
||||
width: 768px;
|
||||
max-width: calc(100% - 2em);
|
||||
color: rgb(var(--gray-dark));
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 1em;
|
||||
padding: 1em 0;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.title h1 {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
.date {
|
||||
margin-bottom: 0.5em;
|
||||
color: rgb(var(--gray));
|
||||
}
|
||||
.last-updated-on {
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { ImageMetadata } from '$lib/utils/types';
|
||||
export let metadata: ImageMetadata[];
|
||||
export let sizes: string;
|
||||
|
||||
const fallback = metadata[metadata.length - 1];
|
||||
const _metadata = metadata.slice(0, metadata.length - 1);
|
||||
|
||||
const srcset = _metadata
|
||||
.map(({ href, width }) => `https://cats.daichendt.one/${href} ${width}w`)
|
||||
.join(',');
|
||||
</script>
|
||||
|
||||
{#if !fallback && !metadata}
|
||||
No metadata supplied
|
||||
{:else}
|
||||
<img {srcset} class="image" alt="A cute kitty" {sizes} loading="lazy" />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<script lang="ts">
|
||||
let copied = false;
|
||||
|
||||
function copyToClipboard(event: MouseEvent) {
|
||||
// @ts-ignore
|
||||
const target = event.target?.innerText.split('\n')[0];
|
||||
navigator.clipboard.writeText(target);
|
||||
|
||||
// select the double clicked node
|
||||
let sel = document.getSelection();
|
||||
let range = new Range();
|
||||
range.selectNode(event.target?.firstChild);
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
if (!copied) {
|
||||
copied = true;
|
||||
|
||||
setTimeout(() => (copied = false), 3000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<code on:dblclick={copyToClipboard}>
|
||||
<span class="text">
|
||||
<slot />
|
||||
</span>
|
||||
<div class:copied class="copyWrapper">
|
||||
{#if copied}
|
||||
Copied
|
||||
{:else}
|
||||
Double click to copy
|
||||
{/if}
|
||||
</div>
|
||||
</code>
|
||||
|
||||
<style>
|
||||
.copied {
|
||||
background-color: yellowgreen !important;
|
||||
min-width: 3rem !important;
|
||||
}
|
||||
|
||||
.copyWrapper {
|
||||
background-color: var(--special-color);
|
||||
margin-bottom: 0.2rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
min-width: 10rem;
|
||||
visibility: hidden;
|
||||
z-index: 1;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-left: 0px;
|
||||
}
|
||||
code:hover .copyWrapper {
|
||||
visibility: visible;
|
||||
}
|
||||
code {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: var(--light-color);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 5px var(--shadow-color);
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border-color);
|
||||
line-break: anywhere;
|
||||
}
|
||||
:global([data-nu-scheme-is='dark'] body code:not([class*='language-'])) {
|
||||
color: var(--bg-color);
|
||||
}
|
||||
:global(pre[class*='language-']) {
|
||||
margin: 0.5em 1rem !important;
|
||||
}
|
||||
:global(code[class*='language-'], pre[class*='language-']) {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<hr />
|
||||
|
||||
<style>
|
||||
hr {
|
||||
background-color: var(--special-color);
|
||||
border: none;
|
||||
height: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
<script context="module">
|
||||
const year = new Date().getFullYear();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { mdiCopyright } from '@mdi/js';
|
||||
|
||||
import Icon from 'mdi-svelte';
|
||||
import Link from './Link.svelte';
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<!-- container class inherited from __layout-->
|
||||
<div class="container">
|
||||
<p>Copyright <Icon path={mdiCopyright} size="1rem" /> {year} Alexander Daichendt</p>
|
||||
|
||||
<div class="footerLinks">
|
||||
<Link href="/cat">Meeeeeow</Link>
|
||||
<Link href="/privacy">Privacy Policy</Link>
|
||||
<Link href="/impressum">Impressum</Link>
|
||||
<Link href="https://github.com/AlexDaichendt/site">Source</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
background-color: var(--special-bg-color);
|
||||
padding: 2rem;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
@media screen and (max-width: 500px) {
|
||||
.footerLinks {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.footerLinks {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
:global(footer div a) {
|
||||
color: var(--text-soft-color) !important;
|
||||
}
|
||||
:global(footer div a:hover) {
|
||||
color: var(--light-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
<script>
|
||||
import ThemeSwitcher from './ThemeSwitcher.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/blog', label: 'Blog' },
|
||||
{ href: '/publications', label: 'Publications' },
|
||||
{ href: '/projects', label: 'Projects' },
|
||||
{ href: '/contact', label: 'Contact' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="header">
|
||||
<a href="/">
|
||||
<h1>Alex Daichendt</h1>
|
||||
</a>
|
||||
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
<nav>
|
||||
<ol class="navList">
|
||||
{#each NAV_ITEMS as navItem}
|
||||
<li
|
||||
class="navItem {$page.url.pathname === navItem.href ||
|
||||
(navItem.href === '/blog' && $page.url.pathname.includes('/blog'))
|
||||
? 'active'
|
||||
: ''}"
|
||||
>
|
||||
<a href={navItem.href}>{navItem.label}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.active {
|
||||
font-weight: 600;
|
||||
}
|
||||
.navList {
|
||||
padding: 0;
|
||||
}
|
||||
.navItem {
|
||||
display: inline;
|
||||
}
|
||||
.navItem:not(:last-child)::after {
|
||||
content: '·';
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.navItem a:hover {
|
||||
color: var(--text-strong-color);
|
||||
}
|
||||
.navItem a:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: 50%;
|
||||
width: 0%;
|
||||
border-bottom: 3px solid var(--outline-color);
|
||||
transition: 0.3s;
|
||||
}
|
||||
.navItem a:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 50%;
|
||||
width: 0%;
|
||||
border-bottom: 3px solid var(--outline-color);
|
||||
transition: 0.3s;
|
||||
}
|
||||
.navItem a:hover:after {
|
||||
width: 50%;
|
||||
}
|
||||
.navItem a:hover:before {
|
||||
width: 50%;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--special-color);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
font-size: 1.2rem;
|
||||
position: relative;
|
||||
}
|
||||
ol {
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* the output of a vite-imagetools import, using the `meta` query for output
|
||||
* format
|
||||
*
|
||||
* full type:
|
||||
* [code](https://github.com/JonasKruckenberg/imagetools/blob/main/packages/core/src/output-formats.ts),
|
||||
* [docs](https://github.com/JonasKruckenberg/imagetools/blob/main/docs/guide/getting-started.md#metadata)
|
||||
*/
|
||||
export let meta: { src: string; width: number; format: string }[];
|
||||
// if there is only one, vite-imagetools won't wrap the object in an array
|
||||
if (!(meta instanceof Array)) meta = [meta];
|
||||
|
||||
// all images by format
|
||||
let sources = new Map<string, typeof meta>();
|
||||
meta.map((m) => sources.set(m.format, []));
|
||||
meta.map((m) => (sources.get(m.format) ?? []).push(m));
|
||||
|
||||
// fallback image: first resolution of last format
|
||||
let image = (sources.get([...sources.keys()].slice(-1)[0]) ?? [])[0];
|
||||
|
||||
/**
|
||||
* `source` attribute. default: width of the first resolution specified in the
|
||||
* import.
|
||||
*/
|
||||
export let sizes = '100vw';
|
||||
|
||||
/** `img` attribute */
|
||||
export let alt: string;
|
||||
|
||||
/** `img` attribute */
|
||||
export let loading: string = 'lazy';
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@component
|
||||
takes the output of a vite-imagetools import (using the `meta` output format)
|
||||
and generates a `<picture>` with `<source>` tags and an `<img>`.
|
||||
|
||||
usage
|
||||
|
||||
- in `global.d.ts`
|
||||
```typescript
|
||||
declare module "*&imagetools" {
|
||||
const out;
|
||||
export default out;
|
||||
}
|
||||
```
|
||||
- in svelte file
|
||||
- typescript
|
||||
```typescript
|
||||
import Image from "$lib/Image.svelte";
|
||||
import me from "$lib/assets/me.jpg?w=200;400&format=webp;png&meta&imagetools";
|
||||
```
|
||||
- html
|
||||
```html
|
||||
<span><Image meta="{me}" alt="me" /></span>
|
||||
```
|
||||
- it's not necessary to wrap it in a `<span>`, but i like to avoid unnested
|
||||
`:global()` selectors in svelte css
|
||||
- scss
|
||||
```scss
|
||||
span :global(img) {
|
||||
border-radius: 50%;
|
||||
}
|
||||
```
|
||||
|
||||
example generated `<picture>`
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source
|
||||
sizes="200px"
|
||||
type="image/webp"
|
||||
srcset="
|
||||
/_app/assets/me-3cfc7c5f.webp 200w,
|
||||
/_app/assets/me-ab564f98.webp 400w
|
||||
"
|
||||
/>
|
||||
<source
|
||||
sizes="200px"
|
||||
type="image/png"
|
||||
srcset="
|
||||
/_app/assets/me-2bc09a6d.png 200w,
|
||||
/_app/assets/me-6f16cc18.png 400w
|
||||
"
|
||||
/>
|
||||
<img src="/_app/assets/me-2bc09a6d.png" alt="me" />
|
||||
</picture>
|
||||
```
|
||||
|
||||
notes
|
||||
|
||||
- from the documentation for
|
||||
[`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes),
|
||||
|
||||
> The selected source size affects the intrinsic size of the image (the
|
||||
> image’s display size if no CSS styling is applied). If the srcset
|
||||
> attribute is absent, or contains no values with a width descriptor, then
|
||||
> the sizes attribute has no effect.
|
||||
|
||||
there are other things that may also affect the intrinsic (and separately,
|
||||
display) size of the image, but this is all we set here.
|
||||
|
||||
- the `&imagetools` in the usage above is to make typescript happy. there are
|
||||
other workarounds, if you'd prefer a differnet one
|
||||
https://github.com/JonasKruckenberg/imagetools/issues/160
|
||||
- it'd be nice if we could just use a plain `<img>` tag, but in my bit of
|
||||
testing that didn't seem to allow for multiple formats. i was also tempted
|
||||
to just use png, but in my bit of testing the webp file was only ~10% (!)
|
||||
the size of the png.
|
||||
|
||||
assumptions
|
||||
|
||||
- this counts on vite-imagetools returning metadata objects in the same order
|
||||
as the query values are specified
|
||||
- e.g. for `?width=100;200&format=webp;png&meta` we expect the source with
|
||||
`width=100` to come before the one with `width=200`, and likewise for
|
||||
`webp` and `png`
|
||||
- i don't think this is guaranteed, so hopefully it doesn't change. looks
|
||||
like it depends on this bit of code
|
||||
https://github.com/JonasKruckenberg/imagetools/blob/main/packages/core/src/lib/resolve-configs.ts#L17
|
||||
|
||||
references
|
||||
|
||||
- responsive images
|
||||
- mdn https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images
|
||||
- css-tricks https://css-tricks.com/a-guide-to-the-responsive-images-syntax-in-html/
|
||||
- web
|
||||
- html
|
||||
- picture https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture
|
||||
- source https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source
|
||||
- img https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img
|
||||
- js
|
||||
- Map https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
|
||||
- ts
|
||||
- wildcard module declarations https://www.typescriptlang.org/docs/handbook/modules.html#wildcard-module-declarations
|
||||
- docs
|
||||
- vite-imagetools https://github.com/JonasKruckenberg/imagetools/tree/main/docs
|
||||
- other
|
||||
- how to generate srcset https://github.com/JonasKruckenberg/imagetools/blob/main/packages/core/src/output-formats.ts
|
||||
- `Map` preserves insertion order https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
|
||||
- you don't set elements on `Map` objects the way you do on regular objects
|
||||
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
|
||||
(this was really hard to figure out lol)
|
||||
- vite-imagetools extensions (to make the import query string shorter)
|
||||
- docs https://github.com/JonasKruckenberg/imagetools/blob/main/docs/guide/extending.md
|
||||
- code (options) https://github.com/JonasKruckenberg/imagetools/blob/main/packages/vite/src/types.ts
|
||||
-->
|
||||
<picture>
|
||||
{#each [...sources.entries()] as [format, meta]}
|
||||
<source
|
||||
{sizes}
|
||||
type="image/{format}"
|
||||
srcset={meta.map((m) => `${m.src} ${m.width}w`).join(', ')}
|
||||
/>
|
||||
{/each}
|
||||
<img src={image.src} {alt} {loading} />
|
||||
</picture>
|
||||
|
||||
<style>
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { mdiLinkVariant } from '@mdi/js';
|
||||
import { mdiChevronRight } from '@mdi/js';
|
||||
import Icon from 'mdi-svelte';
|
||||
export let href: string;
|
||||
export let disableIcon = false;
|
||||
export let disablePrefetch = false;
|
||||
// svelte-ignore unused-export-let
|
||||
export let rel = '';
|
||||
|
||||
const internal = !href.startsWith('http');
|
||||
|
||||
// external props
|
||||
let props: Record<string,string|boolean> = {
|
||||
rel: "nofollow noreferrer noopener",
|
||||
target: "_blank"
|
||||
}
|
||||
if (internal) {
|
||||
// internal props
|
||||
if (!disablePrefetch ){
|
||||
props = {
|
||||
"data-sveltekit-prefetch": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
{...$$props}
|
||||
{...props}
|
||||
{href}
|
||||
>
|
||||
<span class="text"><slot /></span>
|
||||
|
||||
{#if !disableIcon && !internal}
|
||||
<Icon path={internal ? mdiChevronRight : mdiLinkVariant} size="1rem" />
|
||||
{/if}
|
||||
</a><style>
|
||||
a {
|
||||
color: var(--special-color);
|
||||
text-decoration: none;
|
||||
font-weight: 550;
|
||||
}
|
||||
a:hover {
|
||||
background-color: var(--outline-color);
|
||||
color: var(--dark-color)
|
||||
}
|
||||
.text {
|
||||
text-decoration: underline;
|
||||
word-wrap: break-word;
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let id: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<li {id}><slot /></li>
|
||||
|
||||
<style>
|
||||
li {
|
||||
margin: 0.4rem 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { mdiChevronDoubleUp } from '@mdi/js';
|
||||
import Icon from 'mdi-svelte';
|
||||
let y: number = 0;
|
||||
|
||||
$: enabled = y > 100;
|
||||
|
||||
function onClick() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:scrollY={y} />
|
||||
|
||||
{#if enabled}
|
||||
<button on:click={onClick}><Icon path={mdiChevronDoubleUp} /></button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
button:focus {
|
||||
box-shadow: 0 0 5px var(--special-shadow-color);
|
||||
}
|
||||
button {
|
||||
border-radius: 35px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: fixed;
|
||||
bottom: 32px;
|
||||
right: 32px;
|
||||
color: var(--light-color);
|
||||
border: 1px solid var(--special-shadow-color);
|
||||
background-color: var(--dark-color);
|
||||
}
|
||||
button:hover {
|
||||
border: 1px solid var(--shadow-color);
|
||||
color: var(--dark-color);
|
||||
background-color: var(--light-color);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let title = "Alex Daichendt's website";
|
||||
export let keywords: string[] = [];
|
||||
export let description: string = '';
|
||||
let seo = $page.data?.seo;
|
||||
|
||||
if (seo) {
|
||||
title = seo.title ? `${seo.title} - Alex Daichendt` : "Alex Daichendt's website";
|
||||
description = seo.description;
|
||||
keywords = seo.keywords || [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head
|
||||
><title>{title}</title>
|
||||
{#if description.length > 0}
|
||||
<meta name="description" content={description} />
|
||||
{/if}
|
||||
<meta name="author" content="Alexander Daichendt" />
|
||||
{#if keywords.length > 0}
|
||||
<meta name="keywords" content={keywords.join(',')} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
<script>
|
||||
</script>
|
||||
|
||||
<div tabindex="0">
|
||||
<table id="table">
|
||||
<slot />
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0px 0px 2px var(--shadow-color);
|
||||
border: 1px solid var(--outline-color);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(#table thead th) {
|
||||
padding-bottom: 0.25rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
border: solid;
|
||||
width: 350px;
|
||||
margin: auto;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
background: linear-gradient(var(--special-color), var(--special-color)) bottom
|
||||
/* left or right or else */ no-repeat;
|
||||
background-size: 50% 2px;
|
||||
}
|
||||
|
||||
:global(#table tbody tr) {
|
||||
border-bottom: 1px solid var(--outline-color);
|
||||
}
|
||||
|
||||
:global(#table tbody tr:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:global(#table tbody td) {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
<script lang="js">
|
||||
// @ts-nocheck
|
||||
import { onMount } from 'svelte';
|
||||
let checked = false;
|
||||
|
||||
onMount(() => {
|
||||
const ROOT = document.querySelector(':root');
|
||||
const DARK = 'dark';
|
||||
const LIGHT = 'light';
|
||||
const HIGH = 'more';
|
||||
const LOW = 'no-preference';
|
||||
const SCHEMES = [DARK, LIGHT];
|
||||
const CONTRASTS = [HIGH, LOW];
|
||||
function observeContext(data) {
|
||||
if (data.find((record) => !record.attributeName.endsWith('-is'))) {
|
||||
setScheme();
|
||||
setContrast();
|
||||
}
|
||||
}
|
||||
const schemeMedia = matchMedia('(prefers-color-scheme: dark)');
|
||||
const contrastMedia = matchMedia('(prefers-contrast: more)');
|
||||
let globalScheme = schemeMedia.matches ? DARK : LIGHT;
|
||||
let globalContrast = contrastMedia.matches ? HIGH : LOW;
|
||||
schemeMedia.addListener((_media) => {
|
||||
globalScheme = _media.matches ? DARK : LIGHT;
|
||||
setScheme();
|
||||
});
|
||||
contrastMedia.addListener((_media) => {
|
||||
globalContrast = _media.matches ? HIGH : LOW;
|
||||
setContrast();
|
||||
});
|
||||
function setScheme() {
|
||||
const setting = ROOT.dataset.nuScheme;
|
||||
ROOT.dataset.nuSchemeIs =
|
||||
(setting !== 'auto' && SCHEMES.includes(setting) && setting) || globalScheme;
|
||||
}
|
||||
function setContrast() {
|
||||
const setting = ROOT.dataset.nuContrast;
|
||||
ROOT.dataset.nuContrastIs =
|
||||
(setting !== 'auto' && CONTRASTS.includes(setting) && setting) || globalContrast;
|
||||
}
|
||||
const observer = new MutationObserver((data) => observeContext(data));
|
||||
observer.observe(ROOT, {
|
||||
characterData: false,
|
||||
attributes: true,
|
||||
childList: false,
|
||||
subtree: false,
|
||||
});
|
||||
setScheme();
|
||||
// adjust the theme selector
|
||||
checked = globalScheme === DARK;
|
||||
|
||||
setContrast();
|
||||
// Switch to dark scheme
|
||||
// ROOT.dataset.nuContrast = 'more';
|
||||
// Increase contrast
|
||||
// ROOT.dataset.nuScheme = 'dark';
|
||||
});
|
||||
|
||||
function toggleTheme() {
|
||||
const root = document.querySelector(':root');
|
||||
const theme = root.dataset['nuSchemeIs'];
|
||||
|
||||
if (theme === 'light') {
|
||||
root.dataset['nuScheme'] = 'dark';
|
||||
} else {
|
||||
root.dataset['nuScheme'] = 'light';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="switch">
|
||||
<input aria-label="Nightmode" type="checkbox" bind:checked on:change={toggleTheme} />
|
||||
<span class="slider round" />
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: lightskyblue;
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: gold;
|
||||
transition: 0.4s;
|
||||
background: radial-gradient(yellow, orange 63%, transparent calc(63% + 3px) 100%);
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--special-mark-color);
|
||||
}
|
||||
input:checked + .slider:before {
|
||||
background-color: white;
|
||||
background: radial-gradient(circle at 19% 19%, transparent 41%, var(--outline-color) 43%);
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 5px var(--special-shadow-color);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Thumbnail } from '$lib/utils/types';
|
||||
|
||||
export let thumbnail: Thumbnail;
|
||||
</script>
|
||||
|
||||
<svg width="100%" height="100%" viewBox="0 0 {thumbnail.width} {thumbnail.height}">
|
||||
<filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="20 20" edgeMode="duplicate" />
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="discrete" tableValues="1 1" />
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
<image
|
||||
filter="url(#blur)"
|
||||
xlink:href="data:image/jpeg;base64,{thumbnail.value}"
|
||||
x="0"
|
||||
y="0"
|
||||
height="100%"
|
||||
width="100%"
|
||||
/>
|
||||
</svg>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<svg
|
||||
width="1.5rem"
|
||||
height="1.5rem"
|
||||
viewBox="0 0 54 54"
|
||||
style="vertical-align: middle"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M19.4414 3.24C19.4414 1.4506 20.892 0 22.6814 0C34.6108 0 44.2814 9.67065 44.2814 21.6C44.2814 23.3894 42.8308 24.84 41.0414 24.84C39.252 24.84 37.8014 23.3894 37.8014 21.6C37.8014 13.2494 31.032 6.48 22.6814 6.48C20.892 6.48 19.4414 5.0294 19.4414 3.24Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M34.5586 50.76C34.5586 52.5494 33.108 54 31.3186 54C19.3893 54 9.71861 44.3294 9.71861 32.4C9.71861 30.6106 11.1692 29.16 12.9586 29.16C14.748 29.16 16.1986 30.6106 16.1986 32.4C16.1986 40.7505 22.9681 47.52 31.3186 47.52C33.108 47.52 34.5586 48.9706 34.5586 50.76Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.24 34.5601C1.4506 34.5601 -6.34076e-08 33.1095 -1.41625e-07 31.3201C-6.63074e-07 19.3907 9.67065 9.72007 21.6 9.72007C23.3894 9.72007 24.84 11.1707 24.84 12.9601C24.84 14.7495 23.3894 16.2001 21.6 16.2001C13.2495 16.2001 6.48 22.9695 6.48 31.3201C6.48 33.1095 5.0294 34.5601 3.24 34.5601Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M50.76 19.4399C52.5494 19.4399 54 20.8905 54 22.6799C54 34.6093 44.3294 44.2799 32.4 44.2799C30.6106 44.2799 29.16 42.8293 29.16 41.0399C29.16 39.2505 30.6106 37.7999 32.4 37.7999C40.7505 37.7999 47.52 31.0305 47.52 22.6799C47.52 20.8905 48.9706 19.4399 50.76 19.4399Z"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,40 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let title: string;
|
||||
export let authors: string[];
|
||||
export let conference: string;
|
||||
export let pdf: string;
|
||||
</script>
|
||||
|
||||
<li>
|
||||
<a class="main" href={pdf}>
|
||||
<span>
|
||||
{#each authors as author, i}
|
||||
{#if i != authors.length && i != 0}
|
||||
,
|
||||
{/if}
|
||||
{#if author == 'Alexander Daichendt'}
|
||||
<b>{author}</b>
|
||||
{:else}
|
||||
{author}
|
||||
{/if}
|
||||
{/each}
|
||||
</span>
|
||||
|
||||
{title}
|
||||
|
||||
{conference}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let id: string | undefined;
|
||||
</script>
|
||||
|
||||
<sup {id}><slot /></sup>
|
||||
|
||||
<style>
|
||||
sup {
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
top: -0.3em;
|
||||
left: -0.2em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
<script context="module">
|
||||
// @ts-ignore
|
||||
import components from './components';
|
||||
const { a, table, code, li, hr, sup } = components;
|
||||
export { a, table, code, li, hr, sup };
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// @ts-nocheck
|
||||
import { mdiCalendar } from '@mdi/js';
|
||||
import { mdiPencil } from '@mdi/js';
|
||||
import Icon from 'mdi-svelte';
|
||||
import Divider from '$components/Divider.svelte';
|
||||
import '$lib/utils/one-dark.css';
|
||||
import SEO from '$components/SEO.svelte';
|
||||
|
||||
// svelte-ignore unused-export-let
|
||||
export let data;
|
||||
// svelte-ignore unused-export-let
|
||||
export let form;
|
||||
|
||||
export let title;
|
||||
// svelte-ignore unused-export-let
|
||||
export let description;
|
||||
export let created;
|
||||
export let updated = '';
|
||||
// svelte-ignore unused-export-let
|
||||
export let keywords;
|
||||
// svelte-ignore unused-export-let
|
||||
export let hidden = false;
|
||||
</script>
|
||||
|
||||
<SEO {title} {description} {keywords} />
|
||||
|
||||
<h1>{title}</h1>
|
||||
{#if updated || created}
|
||||
<aside role="note">
|
||||
{#if updated}
|
||||
<Icon path={mdiPencil} size="0.8rem" /> updated {new Date(updated).toLocaleDateString(
|
||||
'en-GB',
|
||||
)};
|
||||
{/if}
|
||||
{#if created}
|
||||
<Icon path={mdiCalendar} size="0.8rem" /> created
|
||||
{new Date(created).toLocaleDateString('en-GB')}
|
||||
{/if}
|
||||
</aside>
|
||||
{/if}
|
||||
<Divider />
|
||||
<slot />
|
||||
|
||||
<style>
|
||||
aside {
|
||||
font-weight: 200;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
:global(h1, h2, h3, h4) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
:global(h1 a, h2 a, h3 a, h4 a) {
|
||||
text-decoration: none;
|
||||
font-size: 0.7em;
|
||||
margin-left: 0.5rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out 0.1s;
|
||||
}
|
||||
:global(h1:hover a, h2:hover a, h3:hover a, h4:hover a) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
:global(.icon-link::before) {
|
||||
content: '🔗';
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import Link from '../components/Link.svelte';
|
||||
import Table from '../components/Table.svelte';
|
||||
import Code from '../components/Code.svelte';
|
||||
import ListItem from '../components/ListItem.svelte';
|
||||
import Divider from '../components/Divider.svelte';
|
||||
import sup from '../components/sup.svelte';
|
||||
|
||||
const components = {
|
||||
a: Link,
|
||||
table: Table,
|
||||
code: Code,
|
||||
li: ListItem,
|
||||
hr: Divider,
|
||||
sup,
|
||||
};
|
||||
|
||||
export default components;
|
||||
|
|
@ -1,443 +0,0 @@
|
|||
/**
|
||||
* One Dark theme for prism.js
|
||||
* Based on Atom's One Dark theme: https://github.com/atom/atom/tree/master/packages/one-dark-syntax
|
||||
*/
|
||||
|
||||
/**
|
||||
* One Dark colours (accurate as of commit 8ae45ca on 6 Sep 2018)
|
||||
* From colors.less
|
||||
* --mono-1: hsl(220, 14%, 71%);
|
||||
* --mono-2: hsl(220, 9%, 55%);
|
||||
* --mono-3: hsl(220, 10%, 40%);
|
||||
* --hue-1: hsl(187, 47%, 55%);
|
||||
* --hue-2: hsl(207, 82%, 66%);
|
||||
* --hue-3: hsl(286, 60%, 67%);
|
||||
* --hue-4: hsl(95, 38%, 62%);
|
||||
* --hue-5: hsl(355, 65%, 65%);
|
||||
* --hue-5-2: hsl(5, 48%, 51%);
|
||||
* --hue-6: hsl(29, 54%, 61%);
|
||||
* --hue-6-2: hsl(39, 67%, 69%);
|
||||
* --syntax-fg: hsl(220, 14%, 71%);
|
||||
* --syntax-bg: hsl(220, 13%, 18%);
|
||||
* --syntax-gutter: hsl(220, 14%, 45%);
|
||||
* --syntax-guide: hsla(220, 14%, 71%, 0.15);
|
||||
* --syntax-accent: hsl(220, 100%, 66%);
|
||||
* From syntax-variables.less
|
||||
* --syntax-selection-color: hsl(220, 13%, 28%);
|
||||
* --syntax-gutter-background-color-selected: hsl(220, 13%, 26%);
|
||||
* --syntax-cursor-line: hsla(220, 100%, 80%, 0.04);
|
||||
*/
|
||||
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
background: hsl(220, 13%, 18%);
|
||||
color: hsl(220, 14%, 71%);
|
||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||
font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
code[class*='language-']::-moz-selection,
|
||||
code[class*='language-'] *::-moz-selection,
|
||||
pre[class*='language-'] *::-moz-selection {
|
||||
background: hsl(220, 13%, 28%);
|
||||
color: inherit;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
code[class*='language-']::selection,
|
||||
code[class*='language-'] *::selection,
|
||||
pre[class*='language-'] *::selection {
|
||||
background: hsl(220, 13%, 28%);
|
||||
color: inherit;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*='language-'] {
|
||||
padding: 0.2em 0.3em;
|
||||
border-radius: 0.3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.cdata {
|
||||
color: hsl(220, 10%, 40%);
|
||||
}
|
||||
|
||||
.token.doctype,
|
||||
.token.punctuation,
|
||||
.token.entity {
|
||||
color: hsl(220, 14%, 71%);
|
||||
}
|
||||
|
||||
.token.attr-name,
|
||||
.token.class-name,
|
||||
.token.boolean,
|
||||
.token.constant,
|
||||
.token.number,
|
||||
.token.atrule {
|
||||
color: hsl(29, 54%, 61%);
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: hsl(286, 60%, 67%);
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.symbol,
|
||||
.token.deleted,
|
||||
.token.important {
|
||||
color: hsl(355, 65%, 65%);
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted,
|
||||
.token.regex,
|
||||
.token.attr-value,
|
||||
.token.attr-value > .token.punctuation {
|
||||
color: hsl(95, 38%, 62%);
|
||||
}
|
||||
|
||||
.token.variable,
|
||||
.token.operator,
|
||||
.token.function {
|
||||
color: hsl(207, 82%, 66%);
|
||||
}
|
||||
|
||||
.token.url {
|
||||
color: hsl(187, 47%, 55%);
|
||||
}
|
||||
|
||||
/* HTML overrides */
|
||||
.token.attr-value > .token.punctuation.attr-equals,
|
||||
.token.special-attr > .token.attr-value > .token.value.css {
|
||||
color: hsl(220, 14%, 71%);
|
||||
}
|
||||
|
||||
/* CSS overrides */
|
||||
.language-css .token.selector {
|
||||
color: hsl(355, 65%, 65%);
|
||||
}
|
||||
|
||||
.language-css .token.property {
|
||||
color: hsl(220, 14%, 71%);
|
||||
}
|
||||
|
||||
.language-css .token.function,
|
||||
.language-css .token.url > .token.function {
|
||||
color: hsl(187, 47%, 55%);
|
||||
}
|
||||
|
||||
.language-css .token.url > .token.string.url {
|
||||
color: hsl(95, 38%, 62%);
|
||||
}
|
||||
|
||||
.language-css .token.important,
|
||||
.language-css .token.atrule .token.rule {
|
||||
color: hsl(286, 60%, 67%);
|
||||
}
|
||||
|
||||
/* JS overrides */
|
||||
.language-javascript .token.operator {
|
||||
color: hsl(286, 60%, 67%);
|
||||
}
|
||||
|
||||
.language-javascript
|
||||
.token.template-string
|
||||
> .token.interpolation
|
||||
> .token.interpolation-punctuation.punctuation {
|
||||
color: hsl(5, 48%, 51%);
|
||||
}
|
||||
|
||||
/* JSON overrides */
|
||||
.language-json .token.operator {
|
||||
color: hsl(220, 14%, 71%);
|
||||
}
|
||||
|
||||
.language-json .token.null.keyword {
|
||||
color: hsl(29, 54%, 61%);
|
||||
}
|
||||
|
||||
/* MD overrides */
|
||||
.language-markdown .token.url,
|
||||
.language-markdown .token.url > .token.operator,
|
||||
.language-markdown .token.url-reference.url > .token.string {
|
||||
color: hsl(220, 14%, 71%);
|
||||
}
|
||||
|
||||
.language-markdown .token.url > .token.content {
|
||||
color: hsl(207, 82%, 66%);
|
||||
}
|
||||
|
||||
.language-markdown .token.url > .token.url,
|
||||
.language-markdown .token.url-reference.url {
|
||||
color: hsl(187, 47%, 55%);
|
||||
}
|
||||
|
||||
.language-markdown .token.blockquote.punctuation,
|
||||
.language-markdown .token.hr.punctuation {
|
||||
color: hsl(220, 10%, 40%);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.language-markdown .token.code-snippet {
|
||||
color: hsl(95, 38%, 62%);
|
||||
}
|
||||
|
||||
.language-markdown .token.bold .token.content {
|
||||
color: hsl(29, 54%, 61%);
|
||||
}
|
||||
|
||||
.language-markdown .token.italic .token.content {
|
||||
color: hsl(286, 60%, 67%);
|
||||
}
|
||||
|
||||
.language-markdown .token.strike .token.content,
|
||||
.language-markdown .token.strike .token.punctuation,
|
||||
.language-markdown .token.list.punctuation,
|
||||
.language-markdown .token.title.important > .token.punctuation {
|
||||
color: hsl(355, 65%, 65%);
|
||||
}
|
||||
|
||||
/* General */
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Plugin overrides */
|
||||
/* Selectors should have higher specificity than those in the plugins' default stylesheets */
|
||||
|
||||
/* Show Invisibles plugin overrides */
|
||||
.token.token.tab:not(:empty):before,
|
||||
.token.token.cr:before,
|
||||
.token.token.lf:before,
|
||||
.token.token.space:before {
|
||||
color: hsla(220, 14%, 71%, 0.15);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* Toolbar plugin overrides */
|
||||
/* Space out all buttons and move them away from the right edge of the code block */
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
/* Styling the buttons */
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > button,
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > a,
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > span {
|
||||
background: hsl(220, 13%, 26%);
|
||||
color: hsl(220, 9%, 55%);
|
||||
padding: 0.1em 0.4em;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover,
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus,
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover,
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus,
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover,
|
||||
div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus {
|
||||
background: hsl(220, 13%, 28%);
|
||||
color: hsl(220, 14%, 71%);
|
||||
}
|
||||
|
||||
/* Line Highlight plugin overrides */
|
||||
/* The highlighted line itself */
|
||||
.line-highlight.line-highlight {
|
||||
background: hsla(220, 100%, 80%, 0.04);
|
||||
}
|
||||
|
||||
/* Default line numbers in Line Highlight plugin */
|
||||
.line-highlight.line-highlight:before,
|
||||
.line-highlight.line-highlight[data-end]:after {
|
||||
background: hsl(220, 13%, 26%);
|
||||
color: hsl(220, 14%, 71%);
|
||||
padding: 0.1em 0.6em;
|
||||
border-radius: 0.3em;
|
||||
box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */
|
||||
}
|
||||
|
||||
/* Hovering over a linkable line number (in the gutter area) */
|
||||
/* Requires Line Numbers plugin as well */
|
||||
pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows > span:hover:before {
|
||||
background-color: hsla(220, 100%, 80%, 0.04);
|
||||
}
|
||||
|
||||
/* Line Numbers and Command Line plugins overrides */
|
||||
/* Line separating gutter from coding area */
|
||||
.line-numbers.line-numbers .line-numbers-rows,
|
||||
.command-line .command-line-prompt {
|
||||
border-right-color: hsla(220, 14%, 71%, 0.15);
|
||||
}
|
||||
|
||||
/* Stuff in the gutter */
|
||||
.line-numbers .line-numbers-rows > span:before,
|
||||
.command-line .command-line-prompt > span:before {
|
||||
color: hsl(220, 14%, 45%);
|
||||
}
|
||||
|
||||
/* Match Braces plugin overrides */
|
||||
/* Note: Outline colour is inherited from the braces */
|
||||
.rainbow-braces .token.token.punctuation.brace-level-1,
|
||||
.rainbow-braces .token.token.punctuation.brace-level-5,
|
||||
.rainbow-braces .token.token.punctuation.brace-level-9 {
|
||||
color: hsl(355, 65%, 65%);
|
||||
}
|
||||
|
||||
.rainbow-braces .token.token.punctuation.brace-level-2,
|
||||
.rainbow-braces .token.token.punctuation.brace-level-6,
|
||||
.rainbow-braces .token.token.punctuation.brace-level-10 {
|
||||
color: hsl(95, 38%, 62%);
|
||||
}
|
||||
|
||||
.rainbow-braces .token.token.punctuation.brace-level-3,
|
||||
.rainbow-braces .token.token.punctuation.brace-level-7,
|
||||
.rainbow-braces .token.token.punctuation.brace-level-11 {
|
||||
color: hsl(207, 82%, 66%);
|
||||
}
|
||||
|
||||
.rainbow-braces .token.token.punctuation.brace-level-4,
|
||||
.rainbow-braces .token.token.punctuation.brace-level-8,
|
||||
.rainbow-braces .token.token.punctuation.brace-level-12 {
|
||||
color: hsl(286, 60%, 67%);
|
||||
}
|
||||
|
||||
/* Diff Highlight plugin overrides */
|
||||
/* Taken from https://github.com/atom/github/blob/master/styles/variables.less */
|
||||
pre.diff-highlight > code .token.token.deleted:not(.prefix),
|
||||
pre > code.diff-highlight .token.token.deleted:not(.prefix) {
|
||||
background-color: hsla(353, 100%, 66%, 0.15);
|
||||
}
|
||||
|
||||
pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection,
|
||||
pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection,
|
||||
pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection,
|
||||
pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection {
|
||||
background-color: hsla(353, 95%, 66%, 0.25);
|
||||
}
|
||||
|
||||
pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection,
|
||||
pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection,
|
||||
pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection,
|
||||
pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection {
|
||||
background-color: hsla(353, 95%, 66%, 0.25);
|
||||
}
|
||||
|
||||
pre.diff-highlight > code .token.token.inserted:not(.prefix),
|
||||
pre > code.diff-highlight .token.token.inserted:not(.prefix) {
|
||||
background-color: hsla(137, 100%, 55%, 0.15);
|
||||
}
|
||||
|
||||
pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection,
|
||||
pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection,
|
||||
pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection,
|
||||
pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection {
|
||||
background-color: hsla(135, 73%, 55%, 0.25);
|
||||
}
|
||||
|
||||
pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection,
|
||||
pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection,
|
||||
pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection,
|
||||
pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection {
|
||||
background-color: hsla(135, 73%, 55%, 0.25);
|
||||
}
|
||||
|
||||
/* Previewers plugin overrides */
|
||||
/* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-dark-ui */
|
||||
/* Border around popup */
|
||||
.prism-previewer.prism-previewer:before,
|
||||
.prism-previewer-gradient.prism-previewer-gradient div {
|
||||
border-color: hsl(224, 13%, 17%);
|
||||
}
|
||||
|
||||
/* Angle and time should remain as circles and are hence not included */
|
||||
.prism-previewer-color.prism-previewer-color:before,
|
||||
.prism-previewer-gradient.prism-previewer-gradient div,
|
||||
.prism-previewer-easing.prism-previewer-easing:before {
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
/* Triangles pointing to the code */
|
||||
.prism-previewer.prism-previewer:after {
|
||||
border-top-color: hsl(224, 13%, 17%);
|
||||
}
|
||||
|
||||
.prism-previewer-flipped.prism-previewer-flipped.after {
|
||||
border-bottom-color: hsl(224, 13%, 17%);
|
||||
}
|
||||
|
||||
/* Background colour within the popup */
|
||||
.prism-previewer-angle.prism-previewer-angle:before,
|
||||
.prism-previewer-time.prism-previewer-time:before,
|
||||
.prism-previewer-easing.prism-previewer-easing {
|
||||
background: hsl(219, 13%, 22%);
|
||||
}
|
||||
|
||||
/* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */
|
||||
/* For time, this is the alternate colour */
|
||||
.prism-previewer-angle.prism-previewer-angle circle,
|
||||
.prism-previewer-time.prism-previewer-time circle {
|
||||
stroke: hsl(220, 14%, 71%);
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
/* Stroke colours of the handle, direction point, and vector itself */
|
||||
.prism-previewer-easing.prism-previewer-easing circle,
|
||||
.prism-previewer-easing.prism-previewer-easing path,
|
||||
.prism-previewer-easing.prism-previewer-easing line {
|
||||
stroke: hsl(220, 14%, 71%);
|
||||
}
|
||||
|
||||
/* Fill colour of the handle */
|
||||
.prism-previewer-easing.prism-previewer-easing circle {
|
||||
fill: transparent;
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
export interface BlogPostFrontmatter {
|
||||
created: string;
|
||||
updated?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface BlogPostMeta extends BlogPostFrontmatter {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface ProjectsFrontmatter {
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
inProgress: boolean;
|
||||
}
|
||||
export interface ProjectMeta extends ProjectsFrontmatter {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface Skill {
|
||||
name: string;
|
||||
years: number;
|
||||
started: number;
|
||||
}
|
||||
|
||||
export interface ImageMetadata {
|
||||
href: string;
|
||||
mime: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface Thumbnail {
|
||||
value: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface PageData {
|
||||
seo: {
|
||||
title: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PublicationFrontmatter {
|
||||
created: string;
|
||||
title: string;
|
||||
authors: string[];
|
||||
conference: string;
|
||||
pdf: string; // url to pdf
|
||||
keywords: string[];
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface PublicationMeta extends PublicationFrontmatter {
|
||||
href: string;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
export const createLoadObserver = (handler: () => void) => {
|
||||
let waiting = 0;
|
||||
|
||||
const onload = (el) => {
|
||||
waiting++;
|
||||
console.log(waiting);
|
||||
el.addEventListener('load', () => {
|
||||
waiting--;
|
||||
console.log('waiting ' + waiting);
|
||||
if (waiting === 0) {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return onload;
|
||||
};
|
||||
14
src/modules.d.ts
vendored
|
|
@ -1,14 +0,0 @@
|
|||
declare module 'mdi-svelte' {
|
||||
import { SvelteComponentTyped } from 'svelte';
|
||||
|
||||
export interface IconProps {
|
||||
path: string;
|
||||
size?: number | string;
|
||||
color?: string;
|
||||
flip?: boolean | string;
|
||||
rotate?: number;
|
||||
spin?: number | boolean;
|
||||
title?: string;
|
||||
}
|
||||
export default class Icon extends SvelteComponentTyped<IconProps> {}
|
||||
}
|
||||
34
src/pages/blog/[...slug].astro
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import BlogPost from "../../layouts/BlogPost.astro";
|
||||
import { render } from "astro:content";
|
||||
import Picture from "../../components/Picture.astro";
|
||||
import Ul from "../../components/Ul.astro";
|
||||
import Ol from "../../components/Ol.astro";
|
||||
import Li from "../../components/Li.astro";
|
||||
import Link from "../../components/Link.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection("blog");
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.id },
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
type Props = CollectionEntry<"blog">;
|
||||
|
||||
const post = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
|
||||
const components = {
|
||||
ul: Ul,
|
||||
ol: Ol,
|
||||
li: Li,
|
||||
img: Picture,
|
||||
a: Link,
|
||||
};
|
||||
---
|
||||
|
||||
<BlogPost {...post.data}>
|
||||
<Content components={components} />
|
||||
</BlogPost>
|
||||
33
src/pages/blog/index.astro
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import FormattedDate from "../../components/FormattedDate.astro";
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
|
||||
const posts = (await getCollection("blog")).sort(
|
||||
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||
);
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<section class="max-w-4xl mx-auto">
|
||||
<ul class="">
|
||||
{
|
||||
posts.map((post) => (
|
||||
<li class="hover:dark:bg-gray-700 hover:bg-gray-100 p-2 rounded">
|
||||
<a
|
||||
href={`/blog/${post.id}`}
|
||||
class="grid grid-cols-[2fr,1fr] gap-4 items-center"
|
||||
>
|
||||
<h6 class="text-lg font-medium">
|
||||
{post.data.title}
|
||||
</h6>
|
||||
<span class="date text-right text-gray-600 text-sm">
|
||||
<FormattedDate date={post.data.pubDate} />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
71
src/pages/contact.astro
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
---
|
||||
|
||||
<BaseLayout title="Contact">
|
||||
<ul class="space-y-2">
|
||||
<li class="flex items-center space-x-3">
|
||||
<span class="flex items-center">
|
||||
<Icon name="mdi:email" class="w-6 h-6" />
|
||||
<span class="ml-2 font-medium">E-Mail:</span>
|
||||
</span>
|
||||
<a
|
||||
href="mailto:me@daichendt.one"
|
||||
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
>
|
||||
me@daichendt.one
|
||||
</a>
|
||||
<a
|
||||
href="/pub.key"
|
||||
class="ml-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
>
|
||||
<Icon name="mdi:key" class="w-4 h-4 inline" />
|
||||
PGP key
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex items-center space-x-3">
|
||||
<span class="flex items-center">
|
||||
<Icon name="mdi:github" class="w-6 h-6" />
|
||||
<span class="ml-2 font-medium">Github:</span>
|
||||
</span>
|
||||
<a
|
||||
href="https://github.com/AlexDaichendt"
|
||||
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
>
|
||||
AlexDaichendt
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="flex items-center space-x-3">
|
||||
<span class="flex items-center">
|
||||
<Icon name="simple-icons:element" class="w-6 h-6" />
|
||||
<span class="ml-2 font-medium">Element:</span>
|
||||
</span>
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
@alexdaichendt:matrix.org
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<li class="flex items-center space-x-3">
|
||||
<span class="flex items-center">
|
||||
<Icon name="simple-icons:bluesky" class="w-6 h-6" />
|
||||
<span class="ml-2 font-medium">Bluesky:</span>
|
||||
</span>
|
||||
<a
|
||||
href="https://bsky.app/profile/alexdaichendt.bsky.social"
|
||||
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
|
||||
target="_blank"
|
||||
rel="nofollow noreferrer noopener"
|
||||
>
|
||||
@alexdaichendt.bsky.social
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</BaseLayout>
|
||||
58
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
import Link from "../components/Link.astro";
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<h1>Hi, my name is Alex!</h1>
|
||||
<p>
|
||||
I am a software engineer, Linux enthusiast and a friend of lightweight,
|
||||
resilient systems.
|
||||
</p>
|
||||
<p>
|
||||
My journey in the tech world has been a dynamic one. I've immersed
|
||||
myself in countless projects spanning various video games and, for the
|
||||
past few years, have been maintaining a small homelab, which ignited my
|
||||
passion for DevOps / SRE. I am a privacy enthusiast and advocate for
|
||||
non-invasive software. Occasionally, I channel my creativity into
|
||||
building sleek web applications that prioritize efficiency and adhere to
|
||||
web standards and best practices.
|
||||
</p>
|
||||
<p>
|
||||
I currently work as a software engineer at <Link href="https://tv1.eu"
|
||||
>TV1 GmbH</Link
|
||||
>, a company that provides innovative live streaming and communication
|
||||
solutions. In my free time, I am pursuing my Master's degree in computer
|
||||
science at <Link href="https://www.tum.de/">TUM</Link>, where I
|
||||
contribute to various research papers.
|
||||
</p>
|
||||
|
||||
<h2 class="my-8">Skills</h2>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
||||
<strong>Core stack:</strong><br />
|
||||
Typescript/Node.js, Python, Rust, SQL, vanilla HTML/css
|
||||
</div>
|
||||
|
||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
||||
<strong>Frontend:</strong><br />
|
||||
NextJS, SvelteKit, Astro, Tailwind
|
||||
</div>
|
||||
|
||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
||||
<strong>Ops:</strong><br />
|
||||
Prometheus, Docker, Kubernetes, AWS, Github/lab actions
|
||||
</div>
|
||||
|
||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
||||
<strong>Misc:</strong><br />
|
||||
git, agile, REST, GraphQL, web performance, Linux sysadmin, Vitest
|
||||
</div>
|
||||
|
||||
<div class="flex-[0_1_calc(50%-0.5rem)]">
|
||||
<strong>Languages:</strong><br />
|
||||
German (native), English
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
97
src/pages/publications.astro
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import { publications } from "../data/publications";
|
||||
---
|
||||
|
||||
<BaseLayout title="Publications">
|
||||
<h2>Conference papers</h2>
|
||||
<ul class="space-y-4">
|
||||
{
|
||||
publications.map((pub) => (
|
||||
<li class="rounded-lg border border-gray-200 dark:border-gray-700 p-4 hover:shadow-lg transition-shadow">
|
||||
<div class="space-y-2">
|
||||
<p class="text-lg dark:text-gray-200">
|
||||
{pub.authors.join(", ")}
|
||||
</p>
|
||||
<p class="text-xl font-semibold dark:text-gray-100">
|
||||
"{pub.title}"
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{pub.conference ||
|
||||
pub.journal +
|
||||
", Volume " +
|
||||
pub.volume +
|
||||
", " +
|
||||
pub.date +
|
||||
", " +
|
||||
pub.pages}
|
||||
</p>
|
||||
{pub.location && (
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{pub.location}, {pub.date}
|
||||
</p>
|
||||
)}
|
||||
{pub.links && (
|
||||
<div class="flex gap-4 mt-3">
|
||||
{pub.links.pdf && (
|
||||
<a
|
||||
href={pub.links.pdf}
|
||||
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M9 2a2 2 0 00-2 2v8a2 2 0 002 2h6a2 2 0 002-2V6.414A2 2 0 0016.414 5L14 2.586A2 2 0 0012.586 2H9z" />
|
||||
<path d="M3 8a2 2 0 012-2v10h8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" />
|
||||
</svg>
|
||||
PDF
|
||||
</a>
|
||||
)}
|
||||
{pub.links.homepage && (
|
||||
<a
|
||||
href={pub.links.homepage}
|
||||
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
|
||||
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
|
||||
</svg>
|
||||
Homepage
|
||||
</a>
|
||||
)}
|
||||
{pub.links.bibtex && (
|
||||
<a
|
||||
href={pub.links.bibtex}
|
||||
class="text-mytheme-600 dark:text-mytheme-400 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
BibTeX
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</BaseLayout>
|
||||
16
src/pages/rss.xml.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = await getCollection('blog');
|
||||
return rss({
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
...post.data,
|
||||
link: `/blog/${post.id}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
<script lang="ts">
|
||||
import MoveUpButton from '$components/MoveUpButton.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import '@fontsource/ubuntu-mono/400.css';
|
||||
</script>
|
||||
|
||||
<div class="container upper">
|
||||
<Header />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<MoveUpButton />
|
||||
<Footer />
|
||||
|
||||
<style>
|
||||
@import 'normalize.css';
|
||||
@import 'sanitize.css';
|
||||
|
||||
.upper {
|
||||
min-height: calc(100vh - 4rem - 8px);
|
||||
}
|
||||
|
||||
:global(.container) {
|
||||
max-width: 50rem;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
background-color: var(--subtle-color);
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.3s ease-in;
|
||||
font-family: 'Ubuntu Mono', monospace;
|
||||
line-height: 1.4;
|
||||
overflow-y: scroll;
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
:global([data-nu-scheme-is='light'][data-nu-contrast-is='no-preference'] body) {
|
||||
--bg-color: rgba(255, 255, 255, 1);
|
||||
--text-color: rgba(44, 50, 32, 1);
|
||||
--text-soft-color: rgba(71, 76, 63, 1);
|
||||
--text-strong-color: rgba(74, 74, 74, 1);
|
||||
--subtle-color: rgba(248, 250, 245, 1);
|
||||
--border-color: rgba(225, 234, 210, 1);
|
||||
--shadow-color: rgba(181, 197, 148, 1);
|
||||
--input-color: rgba(255, 255, 255, 1);
|
||||
--outline-color: rgba(249, 201, 163, 1);
|
||||
--mark-color: rgba(224, 146, 38, 0.08);
|
||||
--special-color: rgba(224, 146, 38, 1);
|
||||
--special-bg-color: rgba(229, 149, 39, 1);
|
||||
--special-text-color: rgba(255, 255, 255, 1);
|
||||
--special-shadow-color: rgba(149, 112, 81, 1);
|
||||
--special-mark-color: rgba(255, 255, 255, 0.08);
|
||||
--light-color: rgba(254, 241, 233, 1);
|
||||
--dark-color: rgba(89, 66, 46, 1);
|
||||
--text-color-rgb: 44, 50, 32;
|
||||
--bg-color-rgb: 255, 255, 255;
|
||||
--subtle-color-rgb: 248, 250, 245;
|
||||
--special-color-rgb: 224, 146, 38;
|
||||
--special-text-color-rgb: 255, 255, 255;
|
||||
--special-bg-color-rgb: 229, 149, 39;
|
||||
--shadow-color-rgb: 181, 197, 148;
|
||||
--special-shadow-color-rgb: 149, 112, 81;
|
||||
--outline-color-rgb: 249, 201, 163;
|
||||
--dark-color-rgb: 89, 66, 46;
|
||||
--light-color-rgb: 254, 241, 233;
|
||||
}
|
||||
|
||||
:global(html[data-nu-scheme-is='dark'][data-nu-contrast-is='no-preference'] body) {
|
||||
--bg-color: rgba(32, 32, 32, 1);
|
||||
--text-color: rgba(216, 227, 196, 1);
|
||||
--text-soft-color: rgba(184, 189, 176, 1);
|
||||
--text-strong-color: rgba(187, 187, 187, 1);
|
||||
--subtle-color: rgba(34, 37, 28, 1);
|
||||
--border-color: rgba(44, 51, 27, 1);
|
||||
--shadow-color: rgba(0, 0, 0, 1);
|
||||
--input-color: rgba(28, 28, 28, 1);
|
||||
--outline-color: rgba(217, 151, 82, 1);
|
||||
--mark-color: rgba(170, 113, 47, 0.08);
|
||||
--special-color: rgba(170, 113, 47, 1);
|
||||
--special-bg-color: rgba(151, 100, 40, 1);
|
||||
--special-text-color: rgba(230, 221, 216, 1);
|
||||
--special-shadow-color: rgba(58, 41, 28, 1);
|
||||
--special-mark-color: rgba(230, 221, 216, 0.08);
|
||||
--light-color: rgba(249, 203, 168, 1);
|
||||
--dark-color: rgba(68, 49, 32, 1);
|
||||
--text-color-rgb: 216, 227, 196;
|
||||
--bg-color-rgb: 32, 32, 32;
|
||||
--subtle-color-rgb: 34, 37, 28;
|
||||
--special-color-rgb: 170, 113, 47;
|
||||
--special-text-color-rgb: 230, 221, 216;
|
||||
--special-bg-color-rgb: 151, 100, 40;
|
||||
--shadow-color-rgb: 0, 0, 0;
|
||||
--special-shadow-color-rgb: 58, 41, 28;
|
||||
--outline-color-rgb: 217, 151, 82;
|
||||
--dark-color-rgb: 68, 49, 32;
|
||||
--light-color-rgb: 249, 203, 168;
|
||||
}
|
||||
|
||||
:global(html[data-nu-scheme-is='light'][data-nu-contrast-is='more'] body) {
|
||||
--bg-color: rgba(255, 255, 255, 1);
|
||||
--text-color: rgba(29, 34, 20, 1);
|
||||
--text-soft-color: rgba(31, 33, 26, 1);
|
||||
--text-strong-color: rgba(74, 74, 74, 1);
|
||||
--subtle-color: rgba(248, 250, 245, 1);
|
||||
--border-color: rgba(173, 186, 149, 1);
|
||||
--shadow-color: rgba(147, 162, 117, 1);
|
||||
--input-color: rgba(255, 255, 255, 1);
|
||||
--outline-color: rgba(248, 179, 114, 1);
|
||||
--mark-color: rgba(181, 117, 28, 0.16);
|
||||
--special-color: rgba(181, 117, 28, 1);
|
||||
--special-bg-color: rgba(186, 120, 29, 1);
|
||||
--special-text-color: rgba(255, 255, 255, 1);
|
||||
--special-shadow-color: rgba(85, 63, 44, 1);
|
||||
--special-mark-color: rgba(255, 255, 255, 0.16);
|
||||
--light-color: rgba(254, 241, 233, 1);
|
||||
--dark-color: rgba(89, 66, 46, 1);
|
||||
--text-color-rgb: 29, 34, 20;
|
||||
--bg-color-rgb: 255, 255, 255;
|
||||
--subtle-color-rgb: 248, 250, 245;
|
||||
--special-color-rgb: 181, 117, 28;
|
||||
--special-text-color-rgb: 255, 255, 255;
|
||||
--special-bg-color-rgb: 186, 120, 29;
|
||||
--shadow-color-rgb: 147, 162, 117;
|
||||
--special-shadow-color-rgb: 85, 63, 44;
|
||||
--outline-color-rgb: 248, 179, 114;
|
||||
--dark-color-rgb: 89, 66, 46;
|
||||
--light-color-rgb: 254, 241, 233;
|
||||
}
|
||||
|
||||
:global(html[data-nu-scheme-is='dark'][data-nu-contrast-is='more'] body) {
|
||||
--bg-color: rgba(32, 32, 32, 1);
|
||||
--text-color: rgba(233, 243, 215, 1);
|
||||
--text-soft-color: rgba(236, 241, 227, 1);
|
||||
--text-strong-color: rgba(187, 187, 187, 1);
|
||||
--subtle-color: rgba(34, 37, 28, 1);
|
||||
--border-color: rgba(77, 83, 66, 1);
|
||||
--shadow-color: rgba(0, 0, 0, 1);
|
||||
--input-color: rgba(28, 28, 28, 1);
|
||||
--outline-color: rgba(226, 157, 85, 1);
|
||||
--mark-color: rgba(188, 126, 53, 0.16);
|
||||
--special-color: rgba(188, 126, 53, 1);
|
||||
--special-bg-color: rgba(148, 98, 39, 1);
|
||||
--special-text-color: rgba(243, 238, 236, 1);
|
||||
--special-shadow-color: rgba(0, 0, 0, 1);
|
||||
--special-mark-color: rgba(243, 238, 236, 0.16);
|
||||
--light-color: rgba(251, 223, 203, 1);
|
||||
--dark-color: rgba(68, 49, 32, 1);
|
||||
--text-color-rgb: 233, 243, 215;
|
||||
--bg-color-rgb: 32, 32, 32;
|
||||
--subtle-color-rgb: 34, 37, 28;
|
||||
--special-color-rgb: 188, 126, 53;
|
||||
--special-text-color-rgb: 243, 238, 236;
|
||||
--special-bg-color-rgb: 148, 98, 39;
|
||||
--shadow-color-rgb: 0, 0, 0;
|
||||
--special-shadow-color-rgb: 0, 0, 0;
|
||||
--outline-color-rgb: 226, 157, 85;
|
||||
--dark-color-rgb: 68, 49, 32;
|
||||
--light-color-rgb: 251, 223, 203;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<script lang="ts">
|
||||
import SEO from '$components/SEO.svelte';
|
||||
import ListItem from '$components/ListItem.svelte';
|
||||
import Link from '$components/Link.svelte';
|
||||
|
||||
let SKILLS = [
|
||||
{ name: 'Lightweight virtualisation (LXC, Docker)' },
|
||||
{ name: 'Kubernetes' },
|
||||
{ name: 'Cloud Computing / IaaS (AWS, Cloudflare)' },
|
||||
{ name: 'Web development (fullstack; React, Next, Svelte, vanilla, Rust+WASM)' },
|
||||
{ name: 'Scripting and automation (Bash, Python)' },
|
||||
{ name: 'git and GitOps pipelines' },
|
||||
{ name: 'Linux administration' },
|
||||
];
|
||||
|
||||
const PROJECTS = [
|
||||
{
|
||||
name: 'Lightweight low-latency virtual networking',
|
||||
href: '/blog/tumthesis',
|
||||
description: 'B.Sc. thesis: Evaluate the performance of containers in low-latency networking',
|
||||
},
|
||||
{
|
||||
name: 'This site',
|
||||
href: 'https://github.com/AlexDaichendt/site',
|
||||
description: 'Complete source code for this website',
|
||||
},
|
||||
{
|
||||
name: 'Gear Optimizer (GW2)',
|
||||
href: 'https://optimizer.discretize.eu',
|
||||
description:
|
||||
'MMO Guild Wars 2: Outputs the optimal build given numerous input paramaters; 1000 DAUs',
|
||||
},
|
||||
{
|
||||
name: 'Discretize.eu (GW2)',
|
||||
href: 'https://discretize.eu',
|
||||
description: 'MMO Guild Wars 2: Advanced player guides; 3000 DAUs',
|
||||
},
|
||||
{
|
||||
name: 'discretize-ui (GW2)',
|
||||
href: 'https://github.com/discretize/discretize-ui',
|
||||
description: 'MMO Guild Wars 2: UI library for mirroring in game tooltips with React',
|
||||
},
|
||||
{
|
||||
name: 'Gw2Library (GW2)',
|
||||
href: 'https://gw2library.princeps.biz/',
|
||||
description:
|
||||
'MMO Guild Wars 2: Build library interfacing with the optimizer; CRUD app - NextJS, AWS',
|
||||
},
|
||||
{
|
||||
name: 'LandLord',
|
||||
href: 'https://www.spigotmc.org/resources/landlord-2.44398/',
|
||||
description: 'Minecraft plugin for protecting and managing areas - Java (until 2018)',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<SEO />
|
||||
|
||||
<h1>Hi, my name is Alex!</h1>
|
||||
|
||||
<p>I am a software engineer, Linux enthusiast and a friend of lightweight, resilient systems.</p>
|
||||
<p>
|
||||
My journey in the tech world has been a dynamic one. I've immersed myself in countless projects
|
||||
spanning various video games and, for the past few years, have been maintaining a small homelab,
|
||||
which ignited my passion for DevOps / SRE. I am a privacy enthusiast and advocate for non-invasive
|
||||
software. Occasionally, I channel my creativity into building sleek web applications that
|
||||
prioritize efficiency and adhere to web standards and best practices.
|
||||
</p>
|
||||
<p>
|
||||
Currently, I'm pursuing a Master's degree in computer science at <Link href="https://www.tum.de/"
|
||||
>TUM</Link
|
||||
>, where I successfully contribute to numerous research papers.
|
||||
</p>
|
||||
|
||||
<h2>Skills</h2>
|
||||
|
||||
<ul>
|
||||
{#each SKILLS as skill}
|
||||
<ListItem>
|
||||
{skill.name}
|
||||
</ListItem>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<h2>Highlighted Projects</h2>
|
||||
<ul>
|
||||
{#each PROJECTS as project}
|
||||
<ListItem>
|
||||
<Link href={project.href}>{project.name}</Link> - {project.description}
|
||||
</ListItem>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export async function load() {
|
||||
return {
|
||||
seo: {
|
||||
title: 'Home',
|
||||
description: 'Alex Daichendt"s website, blog, and collection of thoughts on modern tech.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<script lang="ts">
|
||||
let kittyLink: string;
|
||||
let importing = false;
|
||||
let showSuccess = false;
|
||||
|
||||
function beamKitty() {
|
||||
importing = true;
|
||||
|
||||
fetch(`https://cats.daichendt.one/import?url=${kittyLink}&optimize=true`, {
|
||||
method: 'PUT',
|
||||
headers: { 'X-Custom-Auth-Key': import.meta.env.VITE_CATAPI_PASSWD },
|
||||
}).then((result) => {
|
||||
importing = false;
|
||||
|
||||
if (result.status === 200) {
|
||||
kittyLink = '';
|
||||
showSuccess = true;
|
||||
setTimeout(() => (showSuccess = false), 5000);
|
||||
} else {
|
||||
// display error
|
||||
console.log(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1>Administration Area</h1>
|
||||
<h2>Add a cat</h2>
|
||||
<input type="text" placeholder="cat me up" bind:value={kittyLink} />
|
||||
<button type="button" on:click={beamKitty} disabled={importing}> Send </button>
|
||||
|
||||
{#if showSuccess}
|
||||
<span class="success">Successfully beamed up the kitty!</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.success {
|
||||
color: greenyellow;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import type { BlogPostFrontmatter, BlogPostMeta } from '$lib/utils/types';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
const removeExtension = (path: string) => path.replace(/\.[^.]*$/g, '').replace('/+page', '');
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const modulesSVX = import.meta.glob('./**/*.svx');
|
||||
const modulesMD = import.meta.glob('./**/*.md');
|
||||
const modules = { ...modulesMD, ...modulesSVX };
|
||||
const posts: BlogPostMeta[] = [];
|
||||
const resolved = (await Promise.all(Object.values(modules).map((f) => f()))) as {
|
||||
metadata: BlogPostFrontmatter;
|
||||
}[];
|
||||
resolved.forEach((file, index) => {
|
||||
const path = Object.keys(modules)[index];
|
||||
const { metadata } = file;
|
||||
|
||||
if (!metadata?.hidden) posts.push({ ...metadata, href: `blog/${removeExtension(path)}` });
|
||||
});
|
||||
|
||||
posts.sort((a, b) => new Date(b.created).valueOf() - new Date(a.created).valueOf());
|
||||
|
||||
return { posts };
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<script lang="ts">
|
||||
import SEO from '$components/SEO.svelte';
|
||||
import Link from '$components/Link.svelte';
|
||||
import ListItem from '$components/ListItem.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
$: posts = data.posts;
|
||||
</script>
|
||||
|
||||
<SEO />
|
||||
|
||||
<h1>Blog Posts</h1>
|
||||
|
||||
<p>Sometimes I document some of the things I do.</p>
|
||||
|
||||
<ul>
|
||||
{#each posts as post}
|
||||
<ListItem>
|
||||
<Link href={post.href}
|
||||
>{new Date(post.created).toLocaleDateString('en-GB')} - {post.title}</Link
|
||||
>
|
||||
</ListItem>
|
||||
{/each}
|
||||
</ul>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = ({ data }) => {
|
||||
return {
|
||||
posts: data.posts,
|
||||
seo: {
|
||||
title: 'Blog',
|
||||
description:
|
||||
'My blogposts, where I occasionally document things, that I think are not accessible or badly documented.',
|
||||
},
|
||||
};
|
||||
};
|
||||