Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da61cbba5d | |||
| 9037461d27 | |||
| 4a2ea3c32d | |||
| 6db5b36989 | |||
| 38326cc7ec | |||
| aa5fdb579c | |||
| ce27a23c4e | |||
| 141d66d7bb | |||
| de74019e48 | |||
| 32455bf7a7 | |||
| 0c5d9e03b1 | |||
| d34f9f136c | |||
| 22660bed7a | |||
| 7f614d28b5 | |||
| e9d7a994c7 | |||
| f6118aa7a4 | |||
| 5dea6121a3 | |||
| 4e51dd4a83 | |||
| 8e9fcfaeeb | |||
| 2946801517 | |||
| 49028e7783 | |||
| c36cc94437 | |||
| 6d0b4e29d8 | |||
| 7d9b300d84 | |||
| 4718d5df31 | |||
| 153bdf502c | |||
| a7edf0b22a | |||
| a9b73fd08e | |||
| 01f012fc26 | |||
| 94cb2da996 | |||
| 79d3fb142e | |||
| b3fc7b2114 | |||
| a58fafc563 | |||
| bf0910a8fe | |||
| 088a06a51c | |||
| 05a32492ac | |||
| 798027bb9d | |||
| e7cece47f1 | |||
| 34f06b7cfb | |||
| bf7e23b972 | |||
| 9dbea9fff5 | |||
| b50ff60540 | |||
| 9f9d41c9ab | |||
| c612b7d702 | |||
| 8819f31dd0 | |||
| b1faab1f4e | |||
| 7e20081f0d | |||
| 6b484b76b2 | |||
| f152434785 | |||
| ae8775a902 | |||
| 79b4bf43be | |||
| cffa6db131 | |||
| 8753311f5c | |||
| afbf55aa62 |
@@ -0,0 +1,70 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: node:22-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Install SSH and rsync
|
||||||
|
run: apk add --no-cache openssh-client rsync git
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm ci && npm run build
|
||||||
|
|
||||||
|
- name: Setup SSH key
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keyscan -H 10.99.0.23 >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Sync out/ to Portfolio LXC
|
||||||
|
run: |
|
||||||
|
rsync -az --delete \
|
||||||
|
-e "ssh -i ~/.ssh/deploy_key" \
|
||||||
|
out/ root@10.99.0.23:/opt/lerkolabs/out/
|
||||||
|
|
||||||
|
- name: Rebuild and restart container
|
||||||
|
run: |
|
||||||
|
ssh -i ~/.ssh/deploy_key root@10.99.0.23 \
|
||||||
|
"cd /opt/lerkolabs && \
|
||||||
|
docker build -t portfolio . && \
|
||||||
|
docker stop portfolio 2>/dev/null || true && \
|
||||||
|
docker rm portfolio 2>/dev/null || true && \
|
||||||
|
docker run -d --name portfolio -p 80:80 --restart unless-stopped portfolio"
|
||||||
|
|
||||||
|
- name: Tag release (CalVer)
|
||||||
|
run: |
|
||||||
|
git fetch --tags
|
||||||
|
if git describe --exact-match --tags HEAD 2>/dev/null; then
|
||||||
|
echo "Commit already tagged, skipping."
|
||||||
|
else
|
||||||
|
YEAR=$(date +%Y)
|
||||||
|
MONTH=$(date +%m)
|
||||||
|
LATEST=$(git tag --sort=-v:refname | grep -E '^[0-9]{4}\.[0-9]{2}\.[0-9]+$' | head -1)
|
||||||
|
if [ -n "$LATEST" ]; then
|
||||||
|
LATEST_YEAR=$(echo "$LATEST" | cut -d. -f1)
|
||||||
|
LATEST_MONTH=$(echo "$LATEST" | cut -d. -f2)
|
||||||
|
LATEST_MICRO=$(echo "$LATEST" | cut -d. -f3)
|
||||||
|
if [ "$YEAR" = "$LATEST_YEAR" ] && [ "$MONTH" = "$LATEST_MONTH" ]; then
|
||||||
|
MICRO=$((LATEST_MICRO + 1))
|
||||||
|
else
|
||||||
|
MICRO=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
MICRO=1
|
||||||
|
fi
|
||||||
|
NEW_TAG="${YEAR}.$(printf '%02d' $MONTH).${MICRO}"
|
||||||
|
git tag "$NEW_TAG"
|
||||||
|
git push origin "$NEW_TAG"
|
||||||
|
echo "Tagged $NEW_TAG"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm ci && npm run build
|
||||||
|
|
||||||
|
- uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: out/
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- name: Deploy
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# astro
|
||||||
|
/out/
|
||||||
|
/.astro/
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# docs
|
||||||
|
/docs
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
COPY out/ /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Tyler Koenig portfolio
|
||||||
|
|
||||||
|
Personal portfolio site. Live at [lerkolabs.com](https://lerkolabs.com) — self-hosted.
|
||||||
|
|
||||||
|
Source: [gitea.lerkolabs.com/lerko/portfolio](https://gitea.lerkolabs.com/lerko/portfolio)
|
||||||
|
|
||||||
|
**Stack:** Astro 5 · TypeScript · Tailwind v4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branches
|
||||||
|
|
||||||
|
- `dev` — source code; pushing here updates lerkolabs.com
|
||||||
|
- `master` — reserved for future GitHub mirror; don't touch manually
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # dev server at localhost:4321
|
||||||
|
npm run build # static export into out/
|
||||||
|
npm run preview # preview production build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout dev && git merge <branch> && git push gitea dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Push to `dev` triggers Gitea Actions (`.gitea/workflows/deploy.yml`):
|
||||||
|
1. Builds the static site (`npm run build`)
|
||||||
|
2. rsyncs `out/` to the portfolio LXC
|
||||||
|
3. Rebuilds and restarts the Docker container serving lerkolabs.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
layouts/
|
||||||
|
Base.astro # root layout, fonts, theme script, nav/footer
|
||||||
|
pages/
|
||||||
|
index.astro # home: hero, timeline
|
||||||
|
projects.astro # featured + archive projects
|
||||||
|
homelab.astro # VLANs, services, ADRs
|
||||||
|
archive.astro # redirect to /projects/
|
||||||
|
components/ # Nav, Footer, Hero, Timeline, Widget, ProjectCard, Skills
|
||||||
|
data/
|
||||||
|
projects.ts # all projects, featured + archive split
|
||||||
|
services.ts # homelab services with categories
|
||||||
|
timeline.ts # career/project timeline
|
||||||
|
styles/
|
||||||
|
globals.css # full design system (Tailwind v4 CSS-first, all tokens here)
|
||||||
|
public/
|
||||||
|
fonts/ # self-hosted Source Code Pro woff2
|
||||||
|
```
|
||||||
|
|
||||||
|
> Tailwind v4 is CSS-first — no `tailwind.config.ts`. All custom tokens live in `globals.css` under `@theme {}`.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'static',
|
||||||
|
trailingSlash: 'always',
|
||||||
|
outDir: 'out',
|
||||||
|
build: {
|
||||||
|
format: 'directory',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 1001 KiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 4.2 MiB |
@@ -1,428 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200;0,400;0,600;1,200;1,400;1,600&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;700&display=swap');
|
|
||||||
</style>
|
|
||||||
<script src="https://use.fontawesome.com/e427b1b41a.js"></script>
|
|
||||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
|
||||||
<title>Tyler Koenig</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="wrapper">
|
|
||||||
<nav>
|
|
||||||
<a href="index.html">TYLER KOENIG</a>
|
|
||||||
<ul>
|
|
||||||
<li><a href="#bio" target="_self">ABOUT</a></li>
|
|
||||||
<li><a href="#contact" target="_self">CONTACT</a></li>
|
|
||||||
<li>
|
|
||||||
<a href="#project-wrapper" target="_self">PROJECTS</a>
|
|
||||||
</li>
|
|
||||||
<li><a href="#skills" target="_self">SKILLS</a></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<header>
|
|
||||||
<img src="/images/headshot-tyler_koenig.png" alt="tyler" />
|
|
||||||
<h1>
|
|
||||||
<a href="index.html">Tyler Koenig</a>
|
|
||||||
</h1>
|
|
||||||
<h2>
|
|
||||||
Warehouse Associate at Amazon
|
|
||||||
<br />
|
|
||||||
Student at We Can Code IT, Web Developer in the making!
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section id="aboutMe">
|
|
||||||
<div id="bio">
|
|
||||||
<h2>about me</h2>
|
|
||||||
<p>
|
|
||||||
I'm Tyler Koenig, a lifelong student. Graduated with
|
|
||||||
an Associate of Arts from Lorain County Community
|
|
||||||
College in 2018. Inbound associate at Amazon CLE-3
|
|
||||||
since December 2019. Currently learning Java and
|
|
||||||
front-end web development through We Can Code IT
|
|
||||||
Bootcamp. Always interested in a challenge!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div id="contact">
|
|
||||||
<h3>contact</h3>
|
|
||||||
<ul class="contactLinks">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://github.com/lerko96"
|
|
||||||
target="_blank"
|
|
||||||
title="github"
|
|
||||||
>
|
|
||||||
github
|
|
||||||
<i
|
|
||||||
class="fa fa-github fa-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://www.linkedin.com/in/tyler-koenig-72607a18b/"
|
|
||||||
target="_blank"
|
|
||||||
title="LinkedIn"
|
|
||||||
>
|
|
||||||
linkedin
|
|
||||||
<i
|
|
||||||
class="fa fa-linkedin-square fa-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="mailto:tylerkng96@icloud.com"
|
|
||||||
target="_blank"
|
|
||||||
title="tylerkng96@icloud.com"
|
|
||||||
>email
|
|
||||||
<i
|
|
||||||
class="fa fa-envelope-o fa-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="skills">
|
|
||||||
<h4>skills</h4>
|
|
||||||
<ul class="skills">
|
|
||||||
<li>Java</li>
|
|
||||||
<li>Spring</li>
|
|
||||||
<li>Thymeleaf</li>
|
|
||||||
<li>JavaScript</li>
|
|
||||||
<li>MVC</li>
|
|
||||||
<li>HTML</li>
|
|
||||||
<li>CSS</li>
|
|
||||||
<li>Test Driven Development</li>
|
|
||||||
<li>Agile (Scrum)</li>
|
|
||||||
<li>Object Oriented Programming</li>
|
|
||||||
<li>JSON</li>
|
|
||||||
<li>React</li>
|
|
||||||
<li>REST APIs</li>
|
|
||||||
<li>Responsive Design</li>
|
|
||||||
<li>Relational Databases</li>
|
|
||||||
<li>Source Control/ Github</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
<section id="project-wrapper">
|
|
||||||
<h2>projects</h2>
|
|
||||||
<div>
|
|
||||||
<article class="project-item">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a
|
|
||||||
href="https://github.com/pt-spring-2021-team-project-spa/team-2-spa-repo"
|
|
||||||
target="_blank"
|
|
||||||
>Mystery Educator</a
|
|
||||||
>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="project-container">
|
|
||||||
<div class="container-right">
|
|
||||||
<img
|
|
||||||
src="/images/mystery-educator.png"
|
|
||||||
alt="virtual-pet-amok-console"
|
|
||||||
/>
|
|
||||||
<div class="project-desc">
|
|
||||||
<p>
|
|
||||||
Designed a single page application
|
|
||||||
that brings back unique data from
|
|
||||||
MET Museum and NASA APIs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="project-skills">
|
|
||||||
<h4>skills</h4>
|
|
||||||
<ul>
|
|
||||||
<li>Java</li>
|
|
||||||
<li>JavaScript</li>
|
|
||||||
<li>HTML</li>
|
|
||||||
<li>CSS</li>
|
|
||||||
<li>VS Code</li>
|
|
||||||
<li>Github</li>
|
|
||||||
<li>TDD</li>
|
|
||||||
<li>OOP</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="project-item">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a
|
|
||||||
href="https://github.com/2021-Spring-Part-Time/donut-clicker-lerko96"
|
|
||||||
target="_blank"
|
|
||||||
>Donut Clicker</a
|
|
||||||
>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="project-container">
|
|
||||||
<div class="container-right">
|
|
||||||
<img
|
|
||||||
src="/images/donut-clicker.png"
|
|
||||||
alt="virtual-pet-amok-console"
|
|
||||||
/>
|
|
||||||
<div class="project-desc">
|
|
||||||
<p>
|
|
||||||
Designed a single page application
|
|
||||||
that lets users make virtual donuts
|
|
||||||
and buy upgrades for the game.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="project-skills">
|
|
||||||
<h4>skills</h4>
|
|
||||||
<ul>
|
|
||||||
<li>JavaScript</li>
|
|
||||||
<li>HTML</li>
|
|
||||||
<li>CSS</li>
|
|
||||||
<li>VS Code</li>
|
|
||||||
<li>Github</li>
|
|
||||||
<li>TDD</li>
|
|
||||||
<li>OOP</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="project-item">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a
|
|
||||||
href="https://github.com/pt-spring-2021-team-project-fullstack/team-4-trek"
|
|
||||||
target="_blank"
|
|
||||||
>Trekking Website</a
|
|
||||||
>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="project-container">
|
|
||||||
<div class="container-right">
|
|
||||||
<img
|
|
||||||
src="/images/trek.png"
|
|
||||||
alt="virtual-pet-amok-console"
|
|
||||||
/>
|
|
||||||
<div class="project-desc">
|
|
||||||
<p>
|
|
||||||
Designed an MVC website that lets
|
|
||||||
users find treks located by region
|
|
||||||
and continent.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="project-skills">
|
|
||||||
<h4>skills</h4>
|
|
||||||
<ul>
|
|
||||||
<li>Java</li>
|
|
||||||
<li>SpringJPA</li>
|
|
||||||
<li>HTML</li>
|
|
||||||
<li>CSS</li>
|
|
||||||
<li>IntelliJ</li>
|
|
||||||
<li>VS Code</li>
|
|
||||||
<li>Github</li>
|
|
||||||
<li>TDD</li>
|
|
||||||
<li>OOP</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="project-item">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a
|
|
||||||
href="https://github.com/2021-Spring-Part-Time/reviews-mvc-lerko96"
|
|
||||||
target="_blank"
|
|
||||||
>Reviews Site</a
|
|
||||||
>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="project-container">
|
|
||||||
<div class="container-right">
|
|
||||||
<img src="/images/review-site.png" alt="" />
|
|
||||||
<div class="project-desc">
|
|
||||||
<p>
|
|
||||||
Designed a movie reviews website
|
|
||||||
using thymeleaf templates.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="project-skills">
|
|
||||||
<h4>skills</h4>
|
|
||||||
<ul>
|
|
||||||
<li>Java</li>
|
|
||||||
<li>Thymeleaf</li>
|
|
||||||
<li>HTML</li>
|
|
||||||
<li>CSS</li>
|
|
||||||
<li>IntelliJ</li>
|
|
||||||
<li>Github</li>
|
|
||||||
<li>TDD</li>
|
|
||||||
<li>OOP</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="project-item">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a
|
|
||||||
href="https://github.com/2021-Spring-Part-Time/virtual-pets-amok-lerko96"
|
|
||||||
target="_blank"
|
|
||||||
>Virtual Pets Amok</a
|
|
||||||
>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="project-container">
|
|
||||||
<div class="container-right">
|
|
||||||
<img
|
|
||||||
src="/images/amok.png"
|
|
||||||
alt="virtual-pet-amok-console"
|
|
||||||
/>
|
|
||||||
<div class="project-desc">
|
|
||||||
<p>
|
|
||||||
Console App that builds off of
|
|
||||||
virtual-pet-shelter. This add's
|
|
||||||
robotic pets to the shelter, with
|
|
||||||
their own robotic needs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="project-skills">
|
|
||||||
<h4>skills</h4>
|
|
||||||
<ul>
|
|
||||||
<li>TDD</li>
|
|
||||||
<li>polymorphism</li>
|
|
||||||
<li>encapsulation</li>
|
|
||||||
<li>interface (implements)</li>
|
|
||||||
<li>abstract class (extends)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="project-item">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a
|
|
||||||
href="https://github.com/2021-Spring-Part-Time/virtual-pet-shelter-lerko96"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Virtual Pet Shelter</a
|
|
||||||
>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="project-container">
|
|
||||||
<div class="container-right">
|
|
||||||
<img
|
|
||||||
src="/images/shelter.png"
|
|
||||||
alt="virtual-pet-shelter-console"
|
|
||||||
/>
|
|
||||||
<div class="project-desc">
|
|
||||||
<p>
|
|
||||||
Built a console application allowing
|
|
||||||
users to take care of multiple pets.
|
|
||||||
Added the ability to admit pets, or
|
|
||||||
adopt.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="project-skills">
|
|
||||||
<h4>skills</h4>
|
|
||||||
<ul>
|
|
||||||
<li>TDD</li>
|
|
||||||
<li>constructor() {...}</li>
|
|
||||||
<li>getter() {...}</li>
|
|
||||||
<li>map<> = hashmap<>()></li>
|
|
||||||
<li>collections<></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<article class="project-item">
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
<a
|
|
||||||
href="https://github.com/2021-Spring-Part-Time/virtual-pet-lerko96"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Virtual Pet</a
|
|
||||||
>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="project-container">
|
|
||||||
<div class="container-right">
|
|
||||||
<img
|
|
||||||
src="/images/pet.png"
|
|
||||||
alt="virtual-pet-console"
|
|
||||||
/>
|
|
||||||
<div class="project-desc">
|
|
||||||
<p>
|
|
||||||
Built a console application in Java
|
|
||||||
that allows the user to take care of
|
|
||||||
one virtual pet. This includes
|
|
||||||
keeping track of the pets' hunger,
|
|
||||||
thirst and bordedom.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="project-skills">
|
|
||||||
<h4>skills</h4>
|
|
||||||
<ul>
|
|
||||||
<li>game loop</li>
|
|
||||||
<li>user input</li>
|
|
||||||
<li>classes</li>
|
|
||||||
<li>instance variables</li>
|
|
||||||
<li>methods</li>
|
|
||||||
<li>clean code</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<span id="copyright">© 2021 Tyler Koenig</span>
|
|
||||||
<div id="footLinks">
|
|
||||||
<a
|
|
||||||
href="https://github.com/lerko96"
|
|
||||||
target="_blank"
|
|
||||||
title="github"
|
|
||||||
><i class="fa fa-github fa-2x" aria-hidden="true"></i
|
|
||||||
></a>
|
|
||||||
<a
|
|
||||||
href="https://www.linkedin.com/in/tyler-koenig-72607a18b/"
|
|
||||||
target="_blank"
|
|
||||||
title="LinkedIn"
|
|
||||||
><i
|
|
||||||
class="fa fa-linkedin-square fa-2x"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i
|
|
||||||
></a>
|
|
||||||
<a
|
|
||||||
href="mailto:tylerkng96@icloud.com"
|
|
||||||
target="_blank"
|
|
||||||
title="email"
|
|
||||||
><i
|
|
||||||
class="fa fa-envelope-o fa-2x"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i
|
|
||||||
></a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
<script src="index.js" type="text/java"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
console.log('hello nurse');
|
|
||||||
|
|
||||||
var slideIndex = 1;
|
|
||||||
showDivs(slideIndex);
|
|
||||||
|
|
||||||
function plusDivs(n) {
|
|
||||||
showDivs((slideIndex += n));
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDivs(n) {
|
|
||||||
var i;
|
|
||||||
var x = document.getElementsByClassName('mySlides');
|
|
||||||
if (n > x.length) {
|
|
||||||
slideIndex = 1;
|
|
||||||
}
|
|
||||||
if (n < 1) {
|
|
||||||
slideIndex = x.length;
|
|
||||||
}
|
|
||||||
for (i = 0; i < x.length; i++) {
|
|
||||||
x[i].style.display = 'none';
|
|
||||||
}
|
|
||||||
x[slideIndex - 1].style.display = 'block';
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "lerko96-portfolio",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"homepage": "https://www.lerko96.com",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "^5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="border-t border-[var(--color-border)] py-1lh mt-2lh">
|
||||||
|
<div class="px-4ch flex items-center justify-between text-[var(--color-text-dim)]">
|
||||||
|
<span>© {year} Tyler Koenig</span>
|
||||||
|
<nav class="flex items-center gap-2ch">
|
||||||
|
<a
|
||||||
|
href="https://github.com/lerko96"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://gitea.lerkolabs.com/lerko"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
Gitea
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/tyler-koenig"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
LinkedIn
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="mailto:tyler@lerkolabs.com"
|
||||||
|
target="_blank"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
import { services } from "@/data/services";
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="mb-3lh">
|
||||||
|
<h1 class="text-xl font-bold mb-half-lh">Tyler Koenig</h1>
|
||||||
|
<p class="text-[var(--color-text-label)] mb-1lh">
|
||||||
|
Security Operations · Self-Hosted Infrastructure
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-[var(--color-text-label)] leading-relaxed mb-1lh">
|
||||||
|
Homelab runs {services.length} services across segmented VLANs — pfSense, Authentik SSO,
|
||||||
|
full observability stack. Write software too: mobile apps, Go backends, open
|
||||||
|
protocols.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<nav class="flex flex-wrap items-center gap-x-2ch gap-y-half-lh text-[var(--color-text-label)]">
|
||||||
|
<a
|
||||||
|
href="https://github.com/lerko96"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://gitea.lerkolabs.com/lerko"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
Gitea
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/tyler-koenig"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
LinkedIn
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="mailto:tyler@lerkolabs.com"
|
||||||
|
target="_blank"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<header class="sticky top-0 z-50 bg-[var(--color-bg)] border-b border-[var(--color-border)]">
|
||||||
|
<nav class="max-w-[740px] mx-auto px-4ch h-11 flex items-center justify-between">
|
||||||
|
<a href="/" class="font-semibold">Tyler Koenig</a>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2ch">
|
||||||
|
<a href="#projects" data-nav-link class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">projects</a>
|
||||||
|
<a href="#journey" data-nav-link class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">journey</a>
|
||||||
|
<a href="#homelab" data-nav-link class="hidden xs:inline text-[var(--color-text-label)] hover:text-[var(--color-text)]">homelab</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
data-theme-toggle
|
||||||
|
aria-label="Switch to light mode"
|
||||||
|
class="text-[var(--color-text-label)] hover:text-[var(--color-text)] cursor-pointer"
|
||||||
|
>
|
||||||
|
light
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const themeBtn = document.querySelector("[data-theme-toggle]") as HTMLButtonElement;
|
||||||
|
|
||||||
|
function updateTheme() {
|
||||||
|
const isDark = document.documentElement.classList.contains("dark");
|
||||||
|
themeBtn.textContent = isDark ? "light" : "dark";
|
||||||
|
themeBtn.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
isDark ? "Switch to light mode" : "Switch to dark mode",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
themeBtn.addEventListener("click", () => {
|
||||||
|
const next = !document.documentElement.classList.contains("dark");
|
||||||
|
document.documentElement.classList.toggle("dark", next);
|
||||||
|
localStorage.setItem("lerko96-dark-mode", String(next));
|
||||||
|
updateTheme();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTheme();
|
||||||
|
|
||||||
|
const navLinks = document.querySelectorAll<HTMLAnchorElement>("[data-nav-link]");
|
||||||
|
const sections = Array.from(navLinks).map((link) => ({
|
||||||
|
link,
|
||||||
|
section: document.querySelector(link.hash) as HTMLElement,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const atBottom = () =>
|
||||||
|
window.innerHeight + window.scrollY >= document.body.offsetHeight - 2;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
() => {
|
||||||
|
let active: HTMLAnchorElement | null = null;
|
||||||
|
if (atBottom()) {
|
||||||
|
active = sections[sections.length - 1].link;
|
||||||
|
} else {
|
||||||
|
for (const { link, section } of sections) {
|
||||||
|
if (section.getBoundingClientRect().top <= 80) active = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navLinks.forEach((l) => {
|
||||||
|
l.style.color = l === active ? "var(--color-text)" : "";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ rootMargin: "-56px 0px 0px 0px", threshold: 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
sections.forEach(({ section }) => observer.observe(section));
|
||||||
|
|
||||||
|
let ticking = false;
|
||||||
|
window.addEventListener("scroll", () => {
|
||||||
|
if (ticking) return;
|
||||||
|
ticking = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
let active: HTMLAnchorElement | null = null;
|
||||||
|
if (atBottom()) {
|
||||||
|
active = sections[sections.length - 1].link;
|
||||||
|
} else {
|
||||||
|
for (const { link, section } of sections) {
|
||||||
|
if (section.getBoundingClientRect().top <= 80) active = link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navLinks.forEach((l) => {
|
||||||
|
l.style.color = l === active ? "var(--color-text)" : "";
|
||||||
|
});
|
||||||
|
ticking = false;
|
||||||
|
});
|
||||||
|
}, { passive: true });
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import type { Project } from "@/data/projects";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
project: Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<article class="mb-2lh">
|
||||||
|
<div class="flex items-baseline gap-1ch mb-half-lh">
|
||||||
|
<h3>
|
||||||
|
<a
|
||||||
|
href={project.githubUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="font-semibold underline"
|
||||||
|
>
|
||||||
|
{project.title}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
{project.statusBadge && (
|
||||||
|
<span class="text-[var(--color-text-dim)]">({project.statusBadge})</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-[var(--color-text-label)] leading-relaxed mb-half-lh">
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-[var(--color-text-dim)]">
|
||||||
|
{project.tags.join(" · ")}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
import Widget from "./Widget.astro";
|
||||||
|
|
||||||
|
const skillGroups = [
|
||||||
|
{
|
||||||
|
label: "Infrastructure",
|
||||||
|
skills: ["Proxmox", "pfSense", "VLANs", "WireGuard", "Linux", "Caddy"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Desktop & Tools",
|
||||||
|
skills: ["Git", "Docker", "TDD", "Node.js", "REST APIs"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Practices",
|
||||||
|
skills: ["Agile / Scrum", "Relational Databases", "Self-hosting"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Languages",
|
||||||
|
skills: ["Go", "JavaScript", "TypeScript", "HTML", "CSS"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Frontend",
|
||||||
|
skills: ["React", "React Native", "Expo", "Next.js", "Three.js"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalCount = skillGroups.reduce((n, g) => n + g.skills.length, 0);
|
||||||
|
---
|
||||||
|
|
||||||
|
<Widget title="Skills" badge={totalCount} as="section">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{skillGroups.map(({ label, skills }) => (
|
||||||
|
<div class="flex flex-col xs:flex-row gap-1ch xs:gap-2ch py-half-lh">
|
||||||
|
<span class="text-[var(--color-text-dim)] w-28 shrink-0">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{skills.join(" · ")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Widget>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import Widget from "./Widget.astro";
|
||||||
|
import { timeline, type TimelineType } from "@/data/timeline";
|
||||||
|
|
||||||
|
const isDate = (d: string) => /^\d{4}/.test(d);
|
||||||
|
|
||||||
|
const typeLabel: Record<TimelineType, string> = {
|
||||||
|
career: "career",
|
||||||
|
education: "education",
|
||||||
|
cert: "cert",
|
||||||
|
project: "project",
|
||||||
|
homelab: "homelab",
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<Widget title="Journey">
|
||||||
|
<ol class="flex flex-col">
|
||||||
|
{timeline.map((entry) => (
|
||||||
|
<li class="grid grid-cols-[10ch_1fr] gap-2ch py-half-lh border-b border-[var(--color-border)] last:border-b-0">
|
||||||
|
<div class="text-[var(--color-text-dim)] pt-[0.1em]">
|
||||||
|
{isDate(entry.date)
|
||||||
|
? <time datetime={entry.date}>{entry.date}</time>
|
||||||
|
: <span>{entry.date}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
{entry.title}
|
||||||
|
<span class="font-normal text-[var(--color-text-dim)]"> · {typeLabel[entry.type]}</span>
|
||||||
|
</h3>
|
||||||
|
<p class="text-[var(--color-text-label)] leading-relaxed">
|
||||||
|
{entry.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</Widget>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
badge?: string | number;
|
||||||
|
meta?: string;
|
||||||
|
as?: "section" | "div" | "article";
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, badge, meta, as: Tag = "section", class: className } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Tag class:list={["mb-4lh", className]}>
|
||||||
|
<div class="mb-2lh">
|
||||||
|
<h2 class="text-lg font-semibold">
|
||||||
|
{title}
|
||||||
|
{badge !== undefined && (
|
||||||
|
<span class="text-[var(--color-text-dim)] font-normal"> ({badge})</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
{meta && (
|
||||||
|
<p class="text-[var(--color-text-dim)]">{meta}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<slot />
|
||||||
|
</Tag>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
export type Project = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
githubUrl: string;
|
||||||
|
tier: "featured" | "archive";
|
||||||
|
stats?: string;
|
||||||
|
year?: number;
|
||||||
|
statusBadge?: string;
|
||||||
|
externalUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projects: Project[] = [
|
||||||
|
// --- Featured ---
|
||||||
|
{
|
||||||
|
slug: "uptop",
|
||||||
|
title: "uptop",
|
||||||
|
description: "Live uptime monitoring dashboard for your terminal. SSH-accessible. HTTP, ping, TCP, DNS, push checks with alerts, clustering, and Prometheus metrics.",
|
||||||
|
tags: ["Go", "Bubbletea", "Monitoring", "Uptime"],
|
||||||
|
githubUrl: "https://github.com/lerkolabs/uptop",
|
||||||
|
tier: "featured",
|
||||||
|
year: 2026,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "nib",
|
||||||
|
title: "nib",
|
||||||
|
description:
|
||||||
|
"Capture-first personal journal built with Go + SQLite. Currently developing in private when I have spare time.",
|
||||||
|
tags: ["Go", "JavaScript", "SQLite", "Stream-of-Thought"],
|
||||||
|
githubUrl: "https://gitea.lerkolabs.com/lerko/nib-v1",
|
||||||
|
tier: "featured",
|
||||||
|
year: 2026,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "homelab",
|
||||||
|
title: "homelab",
|
||||||
|
description:
|
||||||
|
"7-VLAN segmented network, Wireguard VPN, Proxmox VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).",
|
||||||
|
tags: ["Markdown", "Mermaid", "Proxmox", "Monitor", "Backup"],
|
||||||
|
githubUrl: "https://gitea.lerkolabs.com/lerko/homelab",
|
||||||
|
tier: "featured",
|
||||||
|
year: 2026,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "portfolio",
|
||||||
|
title: "portfolio",
|
||||||
|
description:
|
||||||
|
"Astro static site, self-hosted in a DMZ LXC behind Caddy, deployed via Gitea Actions CI.",
|
||||||
|
tags: ["Astro", "Typescript", "Dockerfile", "Caddy"],
|
||||||
|
githubUrl: "https://gitea.lerkolabs.com/lerko/portfolio",
|
||||||
|
tier: "featured",
|
||||||
|
year: 2021,
|
||||||
|
},
|
||||||
|
// --- Archive ---
|
||||||
|
{
|
||||||
|
slug: "open-pact",
|
||||||
|
title: "open-pact",
|
||||||
|
description: "Open protocol for AI agent identity, delegation, and portable memory. Ed25519 keypair identity, signed delegation",
|
||||||
|
tags: ["TypeScript", "Ed25519", "DID", "npm", "CC0"],
|
||||||
|
githubUrl: "https://github.com/lerko96/open-pact",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2026,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "helm",
|
||||||
|
title: "helm",
|
||||||
|
description:
|
||||||
|
"Full-stack personal productivity dashboard. Go backend with chi router and SQLite, React + TypeScript frontend. Notes, todos, calendar (CalDAV), clipboard, bookmarks, memos. Self-hosted, single-user, daily use.",
|
||||||
|
tags: ["Go", "React", "TypeScript", "SQLite", "CalDAV"],
|
||||||
|
githubUrl: "https://github.com/lerko96/helm",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2026,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "risk-ops",
|
||||||
|
title: "risk-ops",
|
||||||
|
description:
|
||||||
|
"Browser-based strategy dashboard for Risk: Global Domination (SMG Studio). Open one HTML file — no install needed.",
|
||||||
|
tags: ["HTML", "JavaScript"],
|
||||||
|
githubUrl: "#",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2026,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "golf-book-mobile",
|
||||||
|
title: "golf-book-mobile",
|
||||||
|
description:
|
||||||
|
"Offline-first mobile app for tracking golf rounds, managing your 14-club bag, and getting AI-powered club recommendations from a Smart Caddie. Covers 7 shot types per hole with full scorecard history.",
|
||||||
|
tags: ["React Native", "Expo", "Zustand", "AI", "Mobile"],
|
||||||
|
githubUrl: "https://github.com/lerko96/golf-book-mobile",
|
||||||
|
tier: "archive",
|
||||||
|
stats: "200+ commits",
|
||||||
|
statusBadge: "Pending App Store Approval",
|
||||||
|
year: 2025,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "twitter-thread-ext",
|
||||||
|
title: "twitter-thread-ext",
|
||||||
|
description:
|
||||||
|
"Chrome extension (Manifest V3) that captures entire Twitter/X threads and exports them as HTML, Markdown, PDF, or image — with metadata preservation and preview before export.",
|
||||||
|
tags: ["Chrome Extension", "Manifest V3", "JavaScript", "jsPDF"],
|
||||||
|
githubUrl: "https://github.com/lerko96/twitter-thread-ext",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2025,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "notes-app-1.0",
|
||||||
|
title: "notes-app-1.0",
|
||||||
|
description:
|
||||||
|
"Lightweight canvas drawing app with color picker, adjustable brush size, and PNG export. Runs in the browser, no dependencies.",
|
||||||
|
tags: ["HTML5 Canvas", "JavaScript", "CSS"],
|
||||||
|
githubUrl: "https://github.com/lerko96/notes-app-1.0",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2025,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "plaiground",
|
||||||
|
title: "plAIground",
|
||||||
|
description:
|
||||||
|
"Cross-platform desktop AI chat app for developers. Supports OpenAI, Anthropic Claude, and Google Gemini in a single interface with real-time cost tracking, conversation export, and automatic code explanation.",
|
||||||
|
tags: ["Electron", "Node.js", "OpenAI", "Claude", "Gemini"],
|
||||||
|
githubUrl: "https://github.com/lerko96/plaiground",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2025,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "service-monitor",
|
||||||
|
title: "service-monitor",
|
||||||
|
description:
|
||||||
|
"Web dashboard for tracking uptime across multiple services with 30-second polling, status history visualization, JWT-authenticated API, and Docker + nginx deployment.",
|
||||||
|
tags: ["React 18", "Vite", "Express", "SQLite", "Docker", "JWT"],
|
||||||
|
githubUrl: "https://github.com/lerko96/service-monitor",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2025,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "tht-1.2",
|
||||||
|
title: "ThoughtSpace",
|
||||||
|
description:
|
||||||
|
"3D visualization platform for exploring and organizing thoughts using a radio-tuning metaphor. Filter ideas by frequency and bandwidth in an instanced Three.js scene with persistent local storage.",
|
||||||
|
tags: ["React", "TypeScript", "Three.js", "React Three Fiber", "Zustand"],
|
||||||
|
githubUrl: "https://github.com/lerko96/tht-1.2",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2025,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "were-hooked",
|
||||||
|
title: "We're Hooked",
|
||||||
|
description:
|
||||||
|
"Fishing location discovery app built as a team of 5 during bootcamp. Java/Spring MVC backend with Thymeleaf templates.",
|
||||||
|
tags: ["Java", "Spring", "Thymeleaf", "HTML", "CSS"],
|
||||||
|
githubUrl: "https://github.com/lerko96/were-hooked-repo",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2021,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "mystery-educator",
|
||||||
|
title: "Mystery Educator",
|
||||||
|
description:
|
||||||
|
"Single-page app mashup of the MET Museum and NASA public APIs. Built as a team of 4 during bootcamp.",
|
||||||
|
tags: ["JavaScript", "REST APIs", "HTML", "CSS"],
|
||||||
|
githubUrl: "https://github.com/lerko96/mystery-educator",
|
||||||
|
tier: "archive",
|
||||||
|
year: 2021,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const featuredProjects = projects.filter((p) => p.tier === "featured");
|
||||||
|
export const archiveProjects = projects.filter((p) => p.tier === "archive");
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
export type Service = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: "infrastructure" | "security" | "monitoring" | "productivity" | "media";
|
||||||
|
hidden?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const services: Service[] = [
|
||||||
|
// Infrastructure
|
||||||
|
{ name: "pfSense", description: "Firewall, DHCP, routing gateway on Netgate 1100", category: "infrastructure" },
|
||||||
|
{ name: "Caddy", description: "Reverse proxy with automatic wildcard TLS via Cloudflare DNS-01", category: "infrastructure" },
|
||||||
|
{ name: "Pi-hole", description: "Network-wide DNS + ad blocking", category: "infrastructure" },
|
||||||
|
{ name: "WireGuard", description: "VPN — full LAN access for remote clients", category: "infrastructure" },
|
||||||
|
{ name: "mail relay", description: "Outbound SMTP relay for self-hosted service notifications", category: "infrastructure" },
|
||||||
|
{ name: "gluetun", description: "VPN container routing download client traffic", category: "infrastructure", hidden: true },
|
||||||
|
{ name: "Home Assistant", description: "Smart home automation and device management", category: "infrastructure" },
|
||||||
|
|
||||||
|
// Security / Auth
|
||||||
|
{ name: "Authentik", description: "SSO provider — OIDC + forward auth across all services", category: "security" },
|
||||||
|
{ name: "Vaultwarden", description: "Self-hosted password manager, isolated in its own LXC", category: "security" },
|
||||||
|
|
||||||
|
// Monitoring
|
||||||
|
{ name: "Victoria Metrics", description: "Long-term metrics storage and querying", category: "monitoring" },
|
||||||
|
{ name: "Grafana", description: "Dashboards and alerting across all hosts and services", category: "monitoring" },
|
||||||
|
{ name: "Beszel", description: "Lightweight container and host monitoring", category: "monitoring" },
|
||||||
|
{ name: "ntfy", description: "Self-hosted push notifications", category: "monitoring" },
|
||||||
|
{ name: "Uptime Kuma", description: "Self-hosted monitoring tool (GUI)", category: "monitoring" },
|
||||||
|
|
||||||
|
// Productivity
|
||||||
|
{ name: "Gitea", description: "Personal Git server", category: "productivity" },
|
||||||
|
{ name: "Outline", description: "Team wiki and knowledge base", category: "productivity" },
|
||||||
|
{ name: "Actual Budget", description: "Personal budgeting", category: "productivity" },
|
||||||
|
{ name: "Baikal", description: "CalDAV / CardDAV server", category: "productivity" },
|
||||||
|
{ name: "Grist", description: "Spreadsheets and structured data", category: "productivity" },
|
||||||
|
{ name: "Glance", description: "Self-hosted start page with feeds and service status", category: "productivity" },
|
||||||
|
{ name: "Hoarder", description: "Bookmark manager with tagging", category: "productivity", hidden: true },
|
||||||
|
{ name: "FreshRSS", description: "RSS reader", category: "productivity", hidden: true },
|
||||||
|
{ name: "Memos", description: "Quick notes and journal", category: "productivity", hidden: true },
|
||||||
|
{ name: "Ghostfolio", description: "Investment portfolio tracking", category: "productivity", hidden: true },
|
||||||
|
{ name: "Vikunja", description: "Task management", category: "productivity", hidden: true },
|
||||||
|
{ name: "Traggo", description: "Time tracking", category: "productivity", hidden: true },
|
||||||
|
{ name: "Filebrowser", description: "Web-based file manager", category: "productivity", hidden: true },
|
||||||
|
|
||||||
|
// Media
|
||||||
|
{ name: "Immich", description: "Open-source photo library", category: "media"},
|
||||||
|
{ name: "Kavita", description: "Self-hosted manga and book reader", category: "media" },
|
||||||
|
{ name: "Jellyfin", description: "Open-source media streaming", category: "media" },
|
||||||
|
{ name: "Prowlarr", description: "Indexer manager and proxy for the *arr stack", category: "media" },
|
||||||
|
{ name: "*Arr stack", description: "Automated media management for streaming", category: "media" },
|
||||||
|
{ name: "Bazarr", description: "Automatic subtitle download and management", category: "media" },
|
||||||
|
{ name: "Lidarr", description: "Automated music management", category: "media", hidden: true },
|
||||||
|
{ name: "Radarr", description: "Automated movie management", category: "media", hidden: true },
|
||||||
|
{ name: "Plex", description: "Media streaming — movies, TV, music", category: "media", hidden: true },
|
||||||
|
{ name: "nzbget", description: "Usenet downloader", category: "media", hidden: true },
|
||||||
|
{ name: "qBittorrent", description: "Torrent client with web UI", category: "media", hidden: true },
|
||||||
|
{ name: "Openshelf", description: "Book library with auto-ingest", category: "media", hidden: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const categoryOrder: Service["category"][] = [
|
||||||
|
"infrastructure",
|
||||||
|
"security",
|
||||||
|
"monitoring",
|
||||||
|
"productivity",
|
||||||
|
"media",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const categoryLabels: Record<Service["category"], string> = {
|
||||||
|
infrastructure: "Core Infrastructure",
|
||||||
|
security: "Security & Auth",
|
||||||
|
monitoring: "Monitoring",
|
||||||
|
productivity: "Productivity",
|
||||||
|
media: "Media",
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
export type TimelineType =
|
||||||
|
| "career"
|
||||||
|
| "cert"
|
||||||
|
| "project"
|
||||||
|
| "homelab"
|
||||||
|
| "education";
|
||||||
|
|
||||||
|
export interface TimelineEntry {
|
||||||
|
date: string;
|
||||||
|
title: string;
|
||||||
|
type: TimelineType;
|
||||||
|
description: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const timeline: TimelineEntry[] = [
|
||||||
|
{
|
||||||
|
date: "WIP",
|
||||||
|
title: "CompTIA Network+ — in progress",
|
||||||
|
type: "cert",
|
||||||
|
description:
|
||||||
|
"Studying for Network+ to formalize networking knowledge built through the homelab.",
|
||||||
|
tags: ["networking", "certification"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2025",
|
||||||
|
title: "Proxmox Backup Server",
|
||||||
|
type: "homelab",
|
||||||
|
description: "Deployed PBS on used desktop hardware for disaster recovery.",
|
||||||
|
tags: ["backup", "recovery", "retention"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2025",
|
||||||
|
title: "Proxmox Cluster",
|
||||||
|
type: "homelab",
|
||||||
|
description:
|
||||||
|
"Proxmox installed on dedicated server and the fun begins. VMs/LXCs, SSO via Authentik, full monitoring stack (VictoriaMetrics + Grafana + Beszel + ntfy).",
|
||||||
|
tags: ["proxmox", "containers", "VMs", "linux"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2024-06",
|
||||||
|
title: "CompTIA A+",
|
||||||
|
type: "cert",
|
||||||
|
description:
|
||||||
|
"Earned A+ certification, formalizing hardware and OS fundamentals.",
|
||||||
|
tags: ["certification"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2024-03",
|
||||||
|
title: "pfSense",
|
||||||
|
type: "homelab",
|
||||||
|
description:
|
||||||
|
"Netgate 1100 (Marvell ARMADA 3720) picked up on eBay — hands-on networking configuration, VLANs, firewall rules, and troubleshooting.",
|
||||||
|
tags: ["network", "firewall", "vlan", "dhcp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2023-10",
|
||||||
|
title: "SOC Analyst I — Fortress SRM",
|
||||||
|
type: "career",
|
||||||
|
description:
|
||||||
|
"Threat monitoring, incident triage, and client-facing security operations in a managed SOC.",
|
||||||
|
tags: ["soc", "security"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2023-03",
|
||||||
|
title: "Config Tech II — MCPc",
|
||||||
|
type: "career",
|
||||||
|
description:
|
||||||
|
"Promoted to Config Tech II. Led imaging workflows and expanded into scripting for endpoint provisioning.",
|
||||||
|
tags: ["sysadmin", "scripting"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2022-05",
|
||||||
|
title: "Config Tech I — MCPc",
|
||||||
|
type: "career",
|
||||||
|
description:
|
||||||
|
"Hardware configuration, OS imaging, and deployment at scale for enterprise clients.",
|
||||||
|
tags: ["sysadmin", "hardware"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "2021-01",
|
||||||
|
title: "We Can Code IT — Java Bootcamp",
|
||||||
|
type: "education",
|
||||||
|
description:
|
||||||
|
"9-month intensive bootcamp covering Java, OOP, SQL, REST APIs, and Agile development practices.",
|
||||||
|
tags: ["java", "sql", "agile"],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = "Tyler Koenig",
|
||||||
|
description = "Security operations, self-hosted infrastructure, and software projects. Homelab, Go, TypeScript, and more.",
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
var stored = localStorage.getItem("lerko96-dark-mode");
|
||||||
|
var dark = stored === null ? true : stored === "true";
|
||||||
|
if (dark) document.documentElement.classList.add("dark");
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
class="bg-[var(--color-bg)] text-[var(--color-text)] min-h-screen"
|
||||||
|
>
|
||||||
|
<slot name="nav" />
|
||||||
|
<div class="max-w-[740px] mx-auto">
|
||||||
|
<main class="px-4ch py-3lh">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
@import "../styles/globals.css";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Code Pro";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/fonts/SourceCodePro-latin-ext.woff2") format("woff2");
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
|
||||||
|
U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
|
||||||
|
U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Source Code Pro";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/fonts/SourceCodePro-latin.woff2") format("woff2");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
||||||
|
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
|
||||||
|
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="refresh" content="0; url=/#projects" />
|
||||||
|
<link rel="canonical" href="/" />
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>This page moved. <a href="/#projects">/#projects</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="refresh" content="0; url=/#homelab" />
|
||||||
|
<link rel="canonical" href="/" />
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>This page moved. <a href="/#homelab">/#homelab</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
import Base from "@/layouts/Base.astro";
|
||||||
|
import Nav from "@/components/Nav.astro";
|
||||||
|
import Footer from "@/components/Footer.astro";
|
||||||
|
import Hero from "@/components/Hero.astro";
|
||||||
|
import Timeline from "@/components/Timeline.astro";
|
||||||
|
import Widget from "@/components/Widget.astro";
|
||||||
|
import ProjectCard from "@/components/ProjectCard.astro";
|
||||||
|
import { featuredProjects } from "@/data/projects";
|
||||||
|
import { services, categoryOrder, categoryLabels } from "@/data/services";
|
||||||
|
|
||||||
|
const glanceStats = [
|
||||||
|
{ label: "Hypervisor", value: "Proxmox VE" },
|
||||||
|
{ label: "Firewall", value: "pfSense (Netgate 1100)" },
|
||||||
|
{ label: "Network", value: "7 VLANs, default deny, managed switching" },
|
||||||
|
{ label: "Services", value: `${services.length} self-hosted across ${categoryOrder.length} categories` },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<Base>
|
||||||
|
<Nav slot="nav" />
|
||||||
|
|
||||||
|
<Hero />
|
||||||
|
|
||||||
|
<section id="projects">
|
||||||
|
<Widget title="Projects" as="div">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{featuredProjects.map((project) => (
|
||||||
|
<ProjectCard project={project} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Widget>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="journey">
|
||||||
|
<Timeline />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="homelab">
|
||||||
|
<Widget title="Homelab" as="div">
|
||||||
|
<p class="text-[var(--color-text-label)] leading-relaxed max-w-2xl mb-2lh">
|
||||||
|
Personal infrastructure environment for learning, self-hosting, and
|
||||||
|
operational practice. Running 24/7 on production-grade hardware with
|
||||||
|
real network segmentation, SSO, monitoring, and IaC-style
|
||||||
|
documentation.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<dl class="flex flex-col mb-2lh">
|
||||||
|
{glanceStats.map(({ label, value }) => (
|
||||||
|
<div class="flex gap-2ch py-qtr-lh">
|
||||||
|
<dt class="text-[var(--color-text-dim)] w-[16ch] shrink-0">{label}</dt>
|
||||||
|
<dd>{value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-x-[3ch] gap-y-qtr-lh mb-2lh">
|
||||||
|
{categoryOrder.map((cat) => {
|
||||||
|
const count = services.filter((s) => s.category === cat).length;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span class="text-[var(--color-text-dim)]">{categoryLabels[cat]}</span>
|
||||||
|
<span class="text-[var(--color-text-label)]"> ({count})</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-[var(--color-text-label)] mb-half-lh">
|
||||||
|
Full documentation — network maps, ADRs, runbooks, and service configs.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://gitea.lerkolabs.com/lerko/homelab"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
gitea.lerkolabs.com/lerko/homelab →
|
||||||
|
</a>
|
||||||
|
</Widget>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Footer slot="footer" />
|
||||||
|
</Base>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="refresh" content="0; url=/#projects" />
|
||||||
|
<link rel="canonical" href="/" />
|
||||||
|
<title>Redirecting...</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>This page moved. <a href="/#projects">/#projects</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
/* Slate (dark, default) */
|
||||||
|
--color-bg: #1A1B1E;
|
||||||
|
--color-surface: #22242A;
|
||||||
|
--color-surface-raised: #2A2D34;
|
||||||
|
--color-border: #33363E;
|
||||||
|
--color-border-bright: #3E4148;
|
||||||
|
--color-text: #D4D4D8;
|
||||||
|
--color-text-label: #9CA0AA;
|
||||||
|
--color-text-dim: #888D9B;
|
||||||
|
--color-link: #8CAFC8;
|
||||||
|
--color-link-visited: #9A94AB;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-serif: Charter, "Bitstream Charter", "Sitka Text", Cambria, serif;
|
||||||
|
--font-mono: "Source Code Pro", ui-monospace, monospace;
|
||||||
|
/* Breakpoints */
|
||||||
|
--breakpoint-xs: 576px;
|
||||||
|
|
||||||
|
/* Character-grid spacing — horizontal (ch) */
|
||||||
|
--spacing-1ch: 1ch;
|
||||||
|
--spacing-2ch: 2ch;
|
||||||
|
--spacing-3ch: 3ch;
|
||||||
|
--spacing-4ch: 4ch;
|
||||||
|
|
||||||
|
/* Character-grid spacing — vertical (lh, requires line-height:1.5 on html) */
|
||||||
|
--spacing-qtr-lh: 0.25lh;
|
||||||
|
--spacing-half-lh: 0.5lh;
|
||||||
|
--spacing-1lh: 1lh;
|
||||||
|
--spacing-2lh: 2lh;
|
||||||
|
--spacing-3lh: 3lh;
|
||||||
|
--spacing-4lh: 4lh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bone (light) overrides */
|
||||||
|
:root:not(.dark) {
|
||||||
|
--color-bg: #FAF8F5;
|
||||||
|
--color-surface: #F3F0EB;
|
||||||
|
--color-surface-raised: #EBE8E3;
|
||||||
|
--color-border: #E0DCD5;
|
||||||
|
--color-border-bright: #D4D0C8;
|
||||||
|
--color-text: #2C2C2C;
|
||||||
|
--color-text-label: #6B6560;
|
||||||
|
--color-text-dim: #787068;
|
||||||
|
--color-link: #4A6B8A;
|
||||||
|
--color-link-visited: #6B6080;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link underlines */
|
||||||
|
a {
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
text-decoration-color: var(--color-border-bright);
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration-color: currentColor;
|
||||||
|
}
|
||||||
|
a[target="_blank"] {
|
||||||
|
color: var(--color-link);
|
||||||
|
}
|
||||||
|
a[target="_blank"]:visited {
|
||||||
|
color: var(--color-link-visited);
|
||||||
|
}
|
||||||
|
a[target="_blank"]:hover {
|
||||||
|
color: var(--color-link);
|
||||||
|
text-decoration-color: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus states */
|
||||||
|
a:focus-visible {
|
||||||
|
text-decoration-color: currentColor;
|
||||||
|
outline: 2px solid var(--color-border-bright);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid var(--color-border-bright);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default transitions */
|
||||||
|
a,
|
||||||
|
button {
|
||||||
|
transition: color 120ms linear, border-color 120ms linear,
|
||||||
|
opacity 120ms linear, text-decoration-color 120ms linear,
|
||||||
|
outline-color 120ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section anchors — clear sticky nav, visual rhythm */
|
||||||
|
section[id] {
|
||||||
|
scroll-margin-top: 3.5rem;
|
||||||
|
padding-top: 2lh;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
section[id]:first-of-type {
|
||||||
|
border-top: none;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
html {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-size: 11pt;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #000;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
header, footer, button { display: none; }
|
||||||
|
a { color: inherit; text-decoration: underline; }
|
||||||
|
main { max-width: none; padding: 0; }
|
||||||
|
section, div, li { margin-bottom: 0.25em; padding-bottom: 0; }
|
||||||
|
h1, h2, h3 { margin-bottom: 0.15em; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #c9cacc;
|
|
||||||
background-color: #272727;
|
|
||||||
font-size: 16px;
|
|
||||||
font-family: 'Source Code Pro', monospace, Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.725;
|
|
||||||
text-rendering: geometricPrecision;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 560px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: auto;
|
|
||||||
padding: 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 500px;
|
|
||||||
padding: 0.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
color: #c9cacc;
|
|
||||||
letter-spacing: 0.09rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a:hover {
|
|
||||||
background-color: #29f3c3;
|
|
||||||
color: #1b1b1b;
|
|
||||||
transition: 150ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
color: #c9cacc;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
line-height: 1.75rem;
|
|
||||||
border-top: 1.5px solid #666;
|
|
||||||
border-bottom: 1.5px solid #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
header img {
|
|
||||||
width: 100px;
|
|
||||||
border: 5px solid #666;
|
|
||||||
border-radius: 165px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
line-height: 2rem;
|
|
||||||
letter-spacing: 0.01rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 a {
|
|
||||||
font-size: 3rem;
|
|
||||||
color: #c9cacc;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 a:hover {
|
|
||||||
color: #29f3c3;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h2 {
|
|
||||||
font-weight: 200;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
letter-spacing: 0.01rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4 {
|
|
||||||
letter-spacing: 0.01rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#aboutMe {
|
|
||||||
letter-spacing: 0.01rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bio,
|
|
||||||
#skills {
|
|
||||||
padding: 1.5rem;
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bio h2 {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skills h4 {
|
|
||||||
padding: 1.5rem;
|
|
||||||
color: #666;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
}
|
|
||||||
#skills ul {
|
|
||||||
line-height: 1.35rem;
|
|
||||||
font-weight: 200;
|
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skills {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
|
||||||
gap: 10px 75px;
|
|
||||||
grid-auto-flow: row;
|
|
||||||
grid-template-areas:
|
|
||||||
'. .'
|
|
||||||
'. .'
|
|
||||||
'. .'
|
|
||||||
'. .'
|
|
||||||
'. .'
|
|
||||||
'. .'
|
|
||||||
'. .'
|
|
||||||
'. .';
|
|
||||||
}
|
|
||||||
|
|
||||||
#contact {
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#contact h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#contact ul {
|
|
||||||
display: flex;
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contactLinks li {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contactLinks a {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
color: #c9cacc;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contactLinks a:hover {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
#project-wrapper {
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#project-wrapper h2 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 1.5rem 1.5rem 0 1.5rem;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#project-wrapper h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
text-align: left;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1.5rem;
|
|
||||||
letter-spacing: 0.01rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 200;
|
|
||||||
letter-spacing: 0.01rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item a {
|
|
||||||
color: #c9cacc;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item a:hover {
|
|
||||||
color: #29f3c3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-desc {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-skills {
|
|
||||||
color: #666;
|
|
||||||
list-style: none;
|
|
||||||
margin: 0 auto;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
text-align: start;
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-skills h4 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
/* flex: 1; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-skills li {
|
|
||||||
list-style-type: none;
|
|
||||||
color: #c9cacc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-skills ul {
|
|
||||||
/* flex: 9; */
|
|
||||||
line-height: 2rem;
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item img {
|
|
||||||
max-width: 375px;
|
|
||||||
/* width: 90%; */
|
|
||||||
box-shadow: 10px 10px 11px 0px rgba(0, 0, 0, 0.75);
|
|
||||||
-webkit-box-shadow: 10px 10px 11px 0px rgba(0, 0, 0, 0.75);
|
|
||||||
-moz-box-shadow: 10px 10px 11px 0px rgba(0, 0, 0, 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
color: #666;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#copyright {
|
|
||||||
margin: 0.4rem;
|
|
||||||
letter-spacing: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footLinks {
|
|
||||||
margin: 0.4rem;
|
|
||||||
letter-spacing: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footLinks a {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footLinks a:hover {
|
|
||||||
color: #c9cacc;
|
|
||||||
transition: 160ms ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* color: #1b1b1b; */
|
|
||||||
/* color: #2a9d8f */
|
|
||||||
@media (min-width: 576px) {
|
|
||||||
#wrapper {
|
|
||||||
width: 560px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
margin: auto;
|
|
||||||
padding: 0.5rem;
|
|
||||||
flex: 4;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav li {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#aboutMe {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bio {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
#contact {
|
|
||||||
flex: 1;
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contactLinks {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contactLinks li {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
.contactLinks a {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contactLinks i {
|
|
||||||
margin: auto 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skills {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#project-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item img {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-skills {
|
|
||||||
flex: 1;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
text-align: end;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-skills h4 {
|
|
||||||
margin: 1.5rem;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-skills ul {
|
|
||||||
margin: 1.5rem;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-item a {
|
|
||||||
align-self: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 87%;
|
|
||||||
padding: 2rem;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||