Compare commits
36 Commits
c9ad6ff181
...
staging
| 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 |
@@ -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
|
||||
@@ -1,23 +1,17 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
# astro
|
||||
/out/
|
||||
/.astro/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env
|
||||
.env.*
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# docs
|
||||
/docs
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM nginx:alpine
|
||||
COPY out/ /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
@@ -1,70 +1,63 @@
|
||||
# Getting Started with Create React App
|
||||
# Tyler Koenig portfolio
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
Personal portfolio site. Live at [lerkolabs.com](https://lerkolabs.com) — self-hosted.
|
||||
|
||||
## Available Scripts
|
||||
Source: [gitea.lerkolabs.com/lerko/portfolio](https://gitea.lerkolabs.com/lerko/portfolio)
|
||||
|
||||
In the project directory, you can run:
|
||||
**Stack:** Astro 5 · TypeScript · Tailwind v4
|
||||
|
||||
### `npm start`
|
||||
---
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
## Branches
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
- `dev` — source code; pushing here updates lerkolabs.com
|
||||
- `master` — reserved for future GitHub mirror; don't touch manually
|
||||
|
||||
### `npm test`
|
||||
---
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
## Commands
|
||||
|
||||
### `npm run build`
|
||||
```bash
|
||||
npm run dev # dev server at localhost:4321
|
||||
npm run build # static export into out/
|
||||
npm run preview # preview production build
|
||||
```
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
---
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
## Deploy
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
```bash
|
||||
git checkout dev && git merge <branch> && git push gitea dev
|
||||
```
|
||||
|
||||
### `npm run eject`
|
||||
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
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
---
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
## Project layout
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
> 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',
|
||||
},
|
||||
});
|
||||
@@ -1,45 +1,19 @@
|
||||
{
|
||||
"name": "react-scss2",
|
||||
"homepage": "https://lerko96.github.io",
|
||||
"name": "lerko96-portfolio",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"gh-pages": "^3.2.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"homepage": "https://www.lerko96.com",
|
||||
"scripts": {
|
||||
"predeploy": "npm run build",
|
||||
"deploy": "gh-pages -b master -d build",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"dependencies": {
|
||||
"astro": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^6.0.1"
|
||||
"@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 |
@@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!-- Everything I added -->
|
||||
<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=Source+Sans+Pro:wght@300;600;700&display=swap');
|
||||
</style>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;700&display=swap');
|
||||
</style>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css?family=Muli&display=swap');
|
||||
</style>
|
||||
<script src="https://use.fontawesome.com/e427b1b41a.js"></script>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>TK | Portfolio</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
--></body>
|
||||
</html>
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "lerko96 portfolio",
|
||||
"name": "Tyler Koenig Portfolio",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -1,748 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f7f9fb;
|
||||
color: #1b1b1b;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
font-weight: 400;
|
||||
text-align: start;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-transition: background 0.2s linear;
|
||||
transition: background 0.2s linear;
|
||||
}
|
||||
|
||||
body a {
|
||||
color: #1b1b1b;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dark {
|
||||
background: #272727;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.dark #logo a {
|
||||
color: #2bf3c4;
|
||||
}
|
||||
|
||||
.dark a {
|
||||
color: #2bf3c4;
|
||||
}
|
||||
|
||||
.dark .profile__card .card__img #headshot {
|
||||
border: 3px solid #999a9a;
|
||||
}
|
||||
|
||||
.dark .project {
|
||||
background: #1b1b1b;
|
||||
color: #fafafa;
|
||||
-webkit-box-shadow: 0 7px 30px -10px rgba(150, 170, 180, 0.2);
|
||||
box-shadow: 0 7px 30px -10px rgba(150, 170, 180, 0.2);
|
||||
}
|
||||
|
||||
.dark .project .project__title a {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.dark .project .project__info {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dark .project .project__title a {
|
||||
color: #999a9a;
|
||||
}
|
||||
.dark .project .project__info {
|
||||
color: #999a9a;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .project:hover .project__bar {
|
||||
background-color: #c5c6c6;
|
||||
}
|
||||
|
||||
.dark .project:hover .project__title a {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.dark .project:hover .project__info {
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.dark .project .project__tagbox .tag__item {
|
||||
background: #c5c6c6;
|
||||
}
|
||||
|
||||
.dark .project .project__tagbox .tag__item :hover {
|
||||
background: #707171;
|
||||
}
|
||||
|
||||
.App {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.App ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.App {
|
||||
max-width: 990px;
|
||||
}
|
||||
}
|
||||
|
||||
.app__wrapper {
|
||||
margin: auto 1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
line-height: 1.7;
|
||||
padding-top: 30px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) and (prefers-reduced-motion: no-preference) {
|
||||
.App {
|
||||
-webkit-animation: appScale 500ms alternate 1 ease forwards;
|
||||
animation: appScale 500ms alternate 1 ease forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
@-webkit-keyframes appScale {
|
||||
from {
|
||||
-webkit-transform: scale(1.09);
|
||||
transform: scale(1.09);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes appScale {
|
||||
from {
|
||||
-webkit-transform: scale(1.09);
|
||||
transform: scale(1.09);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) and (prefers-reduced-motion: no-preference) {
|
||||
#headshot {
|
||||
-webkit-animation: appScale 333ms alternate 3 ease forwards;
|
||||
animation: appScale 333ms alternate 3 ease forwards;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
@-webkit-keyframes appScale {
|
||||
from {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: scale(1.4);
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
@keyframes appScale {
|
||||
from {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: scale(1.4);
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.profile__about,
|
||||
.profile__skills,
|
||||
#projects {
|
||||
-webkit-animation: lowerApp 1000ms normal 1 ease-out backwards;
|
||||
animation: lowerApp 1000ms normal 1 ease-out backwards;
|
||||
}
|
||||
@-webkit-keyframes lowerApp {
|
||||
from {
|
||||
-webkit-transform: translateY(300px);
|
||||
transform: translateY(300px);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translateY(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
@keyframes lowerApp {
|
||||
from {
|
||||
-webkit-transform: translateY(300px);
|
||||
transform: translateY(300px);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: translateY(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bio__contacts {
|
||||
-webkit-animation: fadeInAnimation ease-out 2000ms;
|
||||
animation: fadeInAnimation ease-out 2000ms;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeInAnimation {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInAnimation {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: justify;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
nav #logo {
|
||||
font-size: 2.3rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2rem;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
nav #logo a {
|
||||
color: #707171;
|
||||
}
|
||||
|
||||
nav #logo a:hover {
|
||||
color: #272727;
|
||||
}
|
||||
|
||||
nav #nav__list {
|
||||
text-align: end;
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.02rem;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #272727;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background-color: #2bf3c4;
|
||||
color: #1b1b1b;
|
||||
-webkit-transition: 150ms ease;
|
||||
transition: 150ms ease;
|
||||
}
|
||||
|
||||
.switch-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.switch-wrap {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 25px;
|
||||
cursor: pointer;
|
||||
background: #4b4b4b;
|
||||
padding: 6px;
|
||||
width: 50px;
|
||||
height: 31px;
|
||||
border-radius: 15.5px;
|
||||
}
|
||||
|
||||
.switch-wrap input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.switch {
|
||||
height: 100%;
|
||||
display: -ms-grid;
|
||||
display: grid;
|
||||
-ms-grid-columns: 0fr 1fr 1fr;
|
||||
grid-template-columns: 0fr 1fr 1fr;
|
||||
-webkit-transition: 0.2s;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.switch::after {
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
-ms-grid-column: 2;
|
||||
grid-column: 2;
|
||||
-webkit-transition: background 0.2s;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
input:checked + .switch {
|
||||
-ms-grid-columns: 1fr 1fr 0fr;
|
||||
grid-template-columns: 1fr 1fr 0fr;
|
||||
}
|
||||
|
||||
input:checked + .switch::after {
|
||||
background-color: #27bb98;
|
||||
}
|
||||
|
||||
#profile h2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile__card {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile__card .card__img {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.profile__card .card__img #headshot {
|
||||
height: 125px;
|
||||
width: 125px;
|
||||
border: 3px solid #272727;
|
||||
border-radius: 50%;
|
||||
-webkit-box-shadow: 0 7px 30px -10px rgba(150, 170, 180, 0.5);
|
||||
box-shadow: 0 7px 30px -10px rgba(150, 170, 180, 0.5);
|
||||
}
|
||||
|
||||
.profile__card .card__bio {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-transition: padding-top 800ms ease;
|
||||
transition: padding-top 800ms ease;
|
||||
}
|
||||
|
||||
.profile__card .card__bio .bio__name {
|
||||
line-height: 0.9em;
|
||||
font-size: 2.3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile__card .card__bio .bio__name a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.profile__card .card__bio .bio__desc {
|
||||
margin: 8px auto 14px;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.profile__card .card__bio .bio__contacts {
|
||||
width: 125px;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-pack: justify;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.profile__card .card__bio .bio__contacts a {
|
||||
color: #c5c6c6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile__card .card__bio .bio__contacts a:hover {
|
||||
color: #707171;
|
||||
-webkit-transition: 150ms ease;
|
||||
transition: 150ms ease;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.profile__card .card__bio {
|
||||
padding-top: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.profile__card .card__bio {
|
||||
-webkit-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.profile__about {
|
||||
margin: 2em auto;
|
||||
line-height: 1.8em;
|
||||
max-width: 764px;
|
||||
-webkit-transition: padding-top 800ms ease;
|
||||
transition: padding-top 800ms ease;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.profile__about {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile__skills ul {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile__skills li {
|
||||
border: 1px solid #707171;
|
||||
border-radius: 2px;
|
||||
padding: 4px 12px;
|
||||
margin: 5px 3px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.profile__skills {
|
||||
margin: 0 auto;
|
||||
max-width: 613px;
|
||||
}
|
||||
}
|
||||
|
||||
#projects {
|
||||
padding: 70px 0 60px;
|
||||
}
|
||||
|
||||
#projects h2 {
|
||||
display: none;
|
||||
color: #707171;
|
||||
font-size: 2.3rem;
|
||||
}
|
||||
|
||||
.project {
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-shadow: 0 7px 30px -10px rgba(150, 170, 180, 0.5);
|
||||
box-shadow: 0 7px 30px -10px rgba(150, 170, 180, 0.5);
|
||||
border-radius: 10px;
|
||||
margin: 3rem auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.project a,
|
||||
.project a:hover {
|
||||
color: #272727;
|
||||
}
|
||||
|
||||
.project h3,
|
||||
.project .h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.project .project__subtitle_small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.project .project__subtitle_small i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.project .project__title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.project .project__title a {
|
||||
-webkit-transition: color 0.1s ease;
|
||||
transition: color 0.1s ease;
|
||||
-webkit-transition: background 250ms ease-in-out;
|
||||
transition: background 250ms ease-in-out;
|
||||
}
|
||||
|
||||
.project .project__title :hover {
|
||||
background: #2bf3c4;
|
||||
}
|
||||
|
||||
.project .project__img {
|
||||
max-height: 180px;
|
||||
width: 100%;
|
||||
-o-object-fit: cover;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.project .project__img_link {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.project .project__bar {
|
||||
width: 50px;
|
||||
height: 8px;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
background-color: #c9cacc;
|
||||
-webkit-transition: background-color 0.1s ease;
|
||||
transition: background-color 0.1s ease;
|
||||
-webkit-transition: width 0.2s ease;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.project .project__text {
|
||||
padding: 1.5rem;
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.project .project__info {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: justify;
|
||||
height: 100%;
|
||||
color: #272727;
|
||||
-webkit-transition: color 0.1s ease;
|
||||
transition: color 0.1s ease;
|
||||
}
|
||||
|
||||
.project .project__tagbox {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-flow: row wrap;
|
||||
flex-flow: row wrap;
|
||||
font-size: 14px;
|
||||
margin: 20px 0 0 0;
|
||||
padding: 0;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.project .project__tagbox .tag__item {
|
||||
display: inline-block;
|
||||
background: #c9cacc;
|
||||
color: #272727;
|
||||
border-radius: 3px;
|
||||
padding: 5px 10px;
|
||||
margin: 0 5px 5px 0;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-transition: background 0.1s ease-in-out;
|
||||
transition: background 0.1s ease-in-out;
|
||||
-webkit-transition: color 0.05s ease-out;
|
||||
transition: color 0.05s ease-out;
|
||||
}
|
||||
|
||||
.project .project__tagbox .tag__item:hover {
|
||||
background: #707171;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.project:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.project:hover .project__bar {
|
||||
width: 100px;
|
||||
background-color: #707171;
|
||||
}
|
||||
|
||||
.project:hover .project__title a {
|
||||
color: #272727;
|
||||
}
|
||||
|
||||
.project:hover .project__info {
|
||||
color: #272727;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.project {
|
||||
-ms-flex-wrap: inherit;
|
||||
flex-wrap: inherit;
|
||||
}
|
||||
.project a {
|
||||
color: #707171;
|
||||
}
|
||||
.project .project__title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
.project .project__tagbox {
|
||||
-webkit-box-pack: start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: start;
|
||||
}
|
||||
.project .project__img {
|
||||
max-width: 250px;
|
||||
max-height: 100%;
|
||||
-webkit-transition: -webkit-transform 0.2s ease;
|
||||
transition: -webkit-transform 0.2s ease;
|
||||
transition: transform 0.2s ease;
|
||||
transition: transform 0.2s ease, -webkit-transform 0.2s ease;
|
||||
}
|
||||
.project .project__text {
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
.project .project__info {
|
||||
color: #707171;
|
||||
}
|
||||
.project .media.project__text:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -20%;
|
||||
height: 130%;
|
||||
width: 55px;
|
||||
}
|
||||
.project:hover .project__img {
|
||||
-webkit-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.project:nth-child(2n + 1) {
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
.project:nth-child(2n + 0) {
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: reverse;
|
||||
-ms-flex-direction: row-reverse;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.project:nth-child(2n + 1) .project__text::before {
|
||||
left: -12px !important;
|
||||
-webkit-transform: rotate(4deg);
|
||||
transform: rotate(4deg);
|
||||
}
|
||||
.project:nth-child(2n + 0) .project__text::before {
|
||||
right: -12px !important;
|
||||
-webkit-transform: rotate(-4deg);
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: reverse;
|
||||
-ms-flex-direction: column-reverse;
|
||||
flex-direction: column-reverse;
|
||||
text-align: center;
|
||||
-webkit-box-pack: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
footer .foot__links {
|
||||
font-size: 2rem;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: space-evenly;
|
||||
-ms-flex-pack: space-evenly;
|
||||
justify-content: space-evenly;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
footer .foot__links a {
|
||||
color: #27bb98;
|
||||
}
|
||||
|
||||
footer .foot__links a :hover {
|
||||
color: #238770;
|
||||
}
|
||||
|
||||
footer .copyright {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
/*# sourceMappingURL=App.css.map */
|
||||
@@ -1,54 +0,0 @@
|
||||
// import logo from './logo.svg';
|
||||
import React from 'react';
|
||||
|
||||
import './App.scss';
|
||||
import Nav from './components/Nav';
|
||||
import Profile from './components/Profile';
|
||||
import Projects from './components/Projects';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
function App() {
|
||||
const [darkMode, setDarkMode] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const json = localStorage.getItem('lerko96-dark-mode');
|
||||
const currentMode = JSON.parse(json);
|
||||
if (currentMode) {
|
||||
setDarkMode(true);
|
||||
} else {
|
||||
setDarkMode(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (darkMode) {
|
||||
document.body.classList.add('dark');
|
||||
} else {
|
||||
document.body.classList.remove('dark');
|
||||
}
|
||||
const json = JSON.stringify(darkMode);
|
||||
localStorage.setItem('lerko96-dark-mode', json);
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<div className='App'>
|
||||
<div class='app__wrapper'>
|
||||
<Nav />
|
||||
<div class='switch-container'>
|
||||
<label class='switch-wrap'>
|
||||
<input
|
||||
type='checkbox'
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
/>
|
||||
<div class='switch'></div>
|
||||
</label>
|
||||
</div>
|
||||
<Profile />
|
||||
<Projects />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,7 +0,0 @@
|
||||
@import './styles/mixins.scss';
|
||||
@import './styles/variables.scss';
|
||||
@import './styles/base.scss';
|
||||
@import './styles/animations.scss';
|
||||
|
||||
@import './styles/components.scss'
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -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>
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Footer = () => (
|
||||
<footer>
|
||||
<span class='copyright'>© 2021 Tyler Koenig</span>
|
||||
<div class='foot__links'>
|
||||
<a
|
||||
href='https://github.com/lerko96'
|
||||
rel='noreferrer'
|
||||
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/'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
title='LinkedIn'
|
||||
>
|
||||
<i class='fa fa-linkedin-square fa-2x' aria-hidden='true'></i>
|
||||
</a>
|
||||
<a
|
||||
href='mailto:tylerkng96@icloud.com'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
title='email'
|
||||
>
|
||||
<i class='fa fa-envelope-o fa-2x' aria-hidden='true'></i>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
export default 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>
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const Nav = () => (
|
||||
<header>
|
||||
<nav>
|
||||
<div id='logo'>
|
||||
<a href='index.html'>tk</a>
|
||||
</div>
|
||||
<div id='nav__list'>
|
||||
<ul>
|
||||
{/* <li id='nav__contact'>
|
||||
<a href='#contact' target='_self'>
|
||||
CONTACT
|
||||
</a>
|
||||
</li> */}
|
||||
<li>
|
||||
<a href='#profile' target='_self'>
|
||||
PROFILE
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='#projects' target='_self'>
|
||||
PROJECTS
|
||||
</a>
|
||||
</li>
|
||||
{/* <li>
|
||||
<button onClick={() => setDarkMode(!darkMode)}>
|
||||
Toggle Dark Mode
|
||||
</button>
|
||||
</li> */}
|
||||
{/* <li>
|
||||
<a href='#skills' target='_self'>
|
||||
SKILLS
|
||||
</a>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
|
||||
export default Nav;
|
||||
@@ -1,96 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import headshot from '../images/headshot-tyler_koenig.png';
|
||||
|
||||
// function Greet() {
|
||||
// return <p>Hello TK</p>
|
||||
// }
|
||||
|
||||
const Profile = () => (
|
||||
<section id='profile'>
|
||||
<article class='profile__card'>
|
||||
<div class='card__img'>
|
||||
<img id='headshot' src={headshot} alt='tyler' />
|
||||
</div>
|
||||
<div class='card__bio'>
|
||||
<div class='bio__name'>
|
||||
<a href='index.html'>TYLER KOENIG</a>
|
||||
</div>
|
||||
<div class='bio__desc'>
|
||||
<p>SOFTWARE DEVELOPER</p>
|
||||
</div>
|
||||
<div class='bio__contacts' id='contact'>
|
||||
<a
|
||||
href='https://www.linkedin.com/in/tyler-koenig-72607a18b/'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
title='LinkedIn'
|
||||
>
|
||||
<i
|
||||
class='fa fa-linkedin-square fa-2x'
|
||||
aria-hidden='true'
|
||||
></i>
|
||||
</a>
|
||||
<a
|
||||
href='https://github.com/lerko96'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
title='github'
|
||||
>
|
||||
<i class='fa fa-github fa-2x' aria-hidden='true'></i>
|
||||
</a>
|
||||
<a
|
||||
href='mailto:tylerkng96@icloud.com'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
title='email'
|
||||
>
|
||||
<i
|
||||
class='fa fa-envelope-o fa-2x'
|
||||
aria-hidden='true'
|
||||
></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class='profile__about'>
|
||||
<h2>about</h2>
|
||||
<p>
|
||||
Full-Stack Java Developer, with a focus on Front-End
|
||||
Development. I graduated with an Associate of Arts from Lorain
|
||||
County Community College in Spring of 2018. I began building
|
||||
HTML, CSS and JavaScript projects by the Summer of 2020 from
|
||||
courses provided on Udemy. I received my Certificate in Software
|
||||
Development from We Can Code IT in Fall of 2021. Thanks to the
|
||||
courses I've taken, I have developed strong skills needed for
|
||||
working in remote team environments, and ones that utilize Scrum
|
||||
and Agile practices. My passion comes from seeing ideas be
|
||||
brought to life. Let's get to work.
|
||||
</p>
|
||||
</article>
|
||||
<article class='profile__skills' id='skills'>
|
||||
<h2>skills</h2>
|
||||
<ul>
|
||||
<li>Java</li>
|
||||
<li>Spring</li>
|
||||
<li>MVC</li>
|
||||
<li>JavaScript</li>
|
||||
<li>JSON</li>
|
||||
<li>Restful APIs</li>
|
||||
<li>Test Driven Development</li>
|
||||
<li>Relational Databases</li>
|
||||
<li>Git</li>
|
||||
<li>Agile/ Scrum</li>
|
||||
<li>HTML</li>
|
||||
<li>CSS</li>
|
||||
<li>SCSS</li>
|
||||
<li>React</li>
|
||||
<li>Responsive Design</li>
|
||||
<li>Thymeleaf</li>
|
||||
{/* <li>Object Oriented Programming</li> */}
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
);
|
||||
|
||||
export default Profile;
|
||||
@@ -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>
|
||||
@@ -1,287 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import wereHooked from '../images/were-hooked.png';
|
||||
import donutClicker from '../images/donut-clicker.png';
|
||||
import mysteryEducator from '../images/mystery-educator.png';
|
||||
import trekkingSite from '../images/trek.png';
|
||||
// import reviewSite from '../images/review-site.png';
|
||||
|
||||
const Projects = () => (
|
||||
<section id='projects'>
|
||||
<h2>projects</h2>
|
||||
|
||||
<article class='project'>
|
||||
<a
|
||||
class='project__img_link'
|
||||
href='https://github.com/lerko96/were-hooked-repo'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
src={wereHooked}
|
||||
class='project__img'
|
||||
alt='were-hooked-img'
|
||||
/>
|
||||
</a>
|
||||
<div class='project__text'>
|
||||
<h3 class='project__title'>
|
||||
<a
|
||||
href='https://github.com/lerko96/were-hooked-repo'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
We're Hooked
|
||||
</a>
|
||||
</h3>
|
||||
<div class='project__subtitle_small'>
|
||||
<time datetime='2021-08-20 12:00:00'>
|
||||
<i class='fa fa-calendar mr-2'></i>Fri, August 20th 2021
|
||||
</time>
|
||||
</div>
|
||||
<div class='project__bar'></div>
|
||||
<div class='project__info'>
|
||||
<p>
|
||||
Designed, built and tested an MVC application that
|
||||
allows users to discover fishing locations in Ohio, as
|
||||
well as teach beginners how to get started fishing and
|
||||
the current regulations that are needed to follow. This
|
||||
project was built between a team of five in a completely
|
||||
remote environment with the help of Github, Zoom and
|
||||
Slack.
|
||||
</p>
|
||||
</div>
|
||||
<ul class='project__tagbox'>
|
||||
<li class='tag__item'>Java</li>
|
||||
<li class='tag__item'>Spring</li>
|
||||
<li class='tag__item'>JavaScript</li>
|
||||
<li class='tag__item'>Restful API</li>
|
||||
<li class='tag__item'>Thymeleaf</li>
|
||||
<li class='tag__item'>HTML</li>
|
||||
<li class='tag__item'>CSS</li>
|
||||
<li class='tag__item'>Responsive Design</li>
|
||||
{/* <li class='tag__item'>TDD</li> */}
|
||||
{/* <li class='tag__item'>VS Code</li> */}
|
||||
<li class='tag__item'>Git</li>
|
||||
<li class='tag__item'>Agile</li>
|
||||
<li class='tag__item'>Scrum</li>
|
||||
<li class='tag__item'>Zoom</li>
|
||||
<li class='tag__item'>Slack</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
<article class='project'>
|
||||
<a
|
||||
class='project__img_link'
|
||||
href='https://github.com/lerko96/mystery-educator'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<img class='project__img' src={mysteryEducator} alt='' />
|
||||
</a>
|
||||
<div class='project__text'>
|
||||
<h3 class='project__title'>
|
||||
<a
|
||||
href='https://github.com/lerko96/mystery-educator'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Mystery Educator
|
||||
</a>
|
||||
</h3>
|
||||
<div class='project__subtitle_small'>
|
||||
<time datetime='2021-07-23 12:00:00'>
|
||||
<i class='fas fa-calendar-alt mr-2'></i>Fri, July 23rd
|
||||
2021
|
||||
</time>
|
||||
</div>
|
||||
<div class='project__bar'></div>
|
||||
<div class='project__info'>
|
||||
<p>
|
||||
Designed, built and tested a single page application
|
||||
that renders unique data from the MET Museum and NASA
|
||||
APIs each time you visit. We also built a local backend
|
||||
database to store historical information. This project
|
||||
was built between a team of four in a completely remote
|
||||
environemnt with the help of Github, Zoom, and Slack.
|
||||
</p>
|
||||
</div>
|
||||
<ul class='project__tagbox'>
|
||||
<li class='tag__item'>JavaScript</li>
|
||||
|
||||
<li class='tag__item'>Node.js</li>
|
||||
<li class='tag__item'>Spring</li>
|
||||
<li class='tag__item'>RESTful APIs</li>
|
||||
<li class='tag__item'>Java</li>
|
||||
<li class='tag__item'>HTML</li>
|
||||
<li class='tag__item'>CSS</li>
|
||||
<li class='tag__item'>Responsive Design</li>
|
||||
{/* <li class='tag__item'>TDD</li> */}
|
||||
{/* <li class='tag__item'>VS Code</li> */}
|
||||
<li class='tag__item'>Git</li>
|
||||
<li class='tag__item'>Agile</li>
|
||||
<li class='tag__item'>Scrum</li>
|
||||
<li class='tag__item'>Zoom</li>
|
||||
<li class='tag__item'>Slack</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
<article class='project'>
|
||||
<a
|
||||
class='project__img_link'
|
||||
href='https://github.com/lerko96/donut-clicker-lerko96'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
class='project__img'
|
||||
src={donutClicker}
|
||||
alt='donut-clicker-img'
|
||||
/>
|
||||
</a>
|
||||
<div class='project__text'>
|
||||
<h3 class='project__title'>
|
||||
<a
|
||||
href='https://github.com/lerko96/donut-clicker-lerko96'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Donut Clicker
|
||||
</a>
|
||||
</h3>
|
||||
<div class='project__subtitle_small'>
|
||||
<time datetime='2021-07-09 12:00:00'>
|
||||
<i class='fas fa-calendar-alt mr-2'></i>Fri, July 9th
|
||||
2021
|
||||
</time>
|
||||
</div>
|
||||
<div class='project__bar'></div>
|
||||
<div class='project__info'>
|
||||
<p>
|
||||
Designed, built and tested a single page application
|
||||
that lets users make virtual donuts via clicking. Once
|
||||
enough donuts are made you have the ability to purchase
|
||||
upgrades such as auto-clickers and clicking-multipliers.
|
||||
</p>
|
||||
</div>
|
||||
<ul class='project__tagbox'>
|
||||
<li class='tag__item'>JavaScript</li>
|
||||
<li class='tag__item'>Node.js</li>
|
||||
<li class='tag__item'>HTML</li>
|
||||
<li class='tag__item'>CSS</li>
|
||||
<li class='tag__item'>Responsive Design</li>
|
||||
<li class='tag__item'>TDD</li>
|
||||
{/* <li class='tag__item'>VS Code</li> */}
|
||||
<li class='tag__item'>Git</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
<article class='project'>
|
||||
<a
|
||||
class='project__img_link'
|
||||
href='https://github.com/lerko96/trek'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
class='project__img'
|
||||
src={trekkingSite}
|
||||
alt='trekking-img'
|
||||
/>
|
||||
</a>
|
||||
<div class='project__text'>
|
||||
<h3 class='project__title'>
|
||||
<a
|
||||
href='https://github.com/lerko96/trek'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Trekking Site
|
||||
</a>
|
||||
</h3>
|
||||
<div class='project__subtitle_small'>
|
||||
<time datetime='2021-06-18 12:00:00'>
|
||||
<i class='fas fa-calendar-alt mr-2'></i>Fri, June 18th
|
||||
2021
|
||||
</time>
|
||||
</div>
|
||||
<div class='project__bar'></div>
|
||||
<div class='project__info'>
|
||||
<p>
|
||||
Designed, built and tested an MVC application that lets
|
||||
users discover treks located by continent, region and
|
||||
type. This project was built between a team of four in a
|
||||
completely remote environemnt with the help of Github,
|
||||
Zoom and Slack.
|
||||
</p>
|
||||
</div>
|
||||
<ul class='project__tagbox'>
|
||||
<li class='tag__item'>Java</li>
|
||||
<li class='tag__item'>Spring</li>
|
||||
{/* <li class='tag__item'>OOP</li> */}
|
||||
<li class='tag__item'>HTML</li>
|
||||
<li class='tag__item'>CSS</li>
|
||||
<li class='tag__item'>Responsive Design</li>
|
||||
<li class='tag__item'>TDD</li>
|
||||
{/* <li class='tag__item'>IntelliJ</li> */}
|
||||
<li class='tag__item'>Git</li>
|
||||
<li class='tag__item'>Agile</li>
|
||||
<li class='tag__item'>Scrum</li>
|
||||
<li class='tag__item'>Zoom</li>
|
||||
<li class='tag__item'>Slack</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
{/* <article class='project'>
|
||||
<a
|
||||
class='project__img_link'
|
||||
href='https://github.com/lerko96/reviews-mvc'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<img
|
||||
class='project__img'
|
||||
src={reviewSite}
|
||||
alt='review-site-img'
|
||||
/>
|
||||
</a>
|
||||
<div class='project__text'>
|
||||
<h3 class='project__title'>
|
||||
<a
|
||||
href='https://github.com/lerko96/reviews-mvc'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
Movie Reviews
|
||||
</a>
|
||||
</h3>
|
||||
<div class='project__subtitle_small'>
|
||||
<time datetime='2021-06-04 12:00:00'>
|
||||
<i class='fas fa-calendar-alt mr-2'></i>Fri, June 4th
|
||||
2021
|
||||
</time>
|
||||
</div>
|
||||
<div class='project__bar'></div>
|
||||
<div class='project__info'>
|
||||
<p>
|
||||
Designed, built and tested a Java application for movie
|
||||
reviews.
|
||||
</p>
|
||||
</div>
|
||||
<ul class='project__tagbox'>
|
||||
<li class='tag__item'>Java</li>
|
||||
<li class='tag__item'>SpringJPA</li>
|
||||
<li class='tag__item'>OOP</li>
|
||||
<li class='tag__item'>Thymeleaf</li>
|
||||
<li class='tag__item'>HTML</li>
|
||||
<li class='tag__item'>CSS</li>
|
||||
<li class='tag__item'>Responsive Design</li>
|
||||
<li class='tag__item'>TDD</li>
|
||||
<li class='tag__item'>IntelliJ</li>
|
||||
<li class='tag__item'>Github</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article> */}
|
||||
</section>
|
||||
);
|
||||
|
||||
export default Projects;
|
||||
@@ -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"],
|
||||
},
|
||||
];
|
||||
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 1001 KiB |
|
Before Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 394 KiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 829 KiB |
@@ -1,13 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
@@ -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>
|
||||
@@ -1,13 +0,0 @@
|
||||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -1,73 +0,0 @@
|
||||
.App {
|
||||
@include respond-below(xs) {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: appScale 500ms alternate 1 ease forwards;
|
||||
}
|
||||
@keyframes appScale {
|
||||
from {
|
||||
transform: scale(1.09);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#headshot {
|
||||
@include respond-above(xs) {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
// animation: App-logo-spin 1 1250ms ease-in-out;
|
||||
animation: appScale 333ms alternate 3 ease forwards;
|
||||
}
|
||||
|
||||
// @keyframes App-logo-spin {
|
||||
// from {
|
||||
// transform: rotateY(0deg);
|
||||
// }
|
||||
// to {
|
||||
// transform: rotateY(360deg);
|
||||
// }
|
||||
// }
|
||||
|
||||
@keyframes appScale {
|
||||
from {
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile__about,
|
||||
.profile__skills,
|
||||
#projects {
|
||||
@include respond-below(xs) {
|
||||
animation: lowerApp 1000ms normal 1 ease-out backwards;
|
||||
@keyframes lowerApp {
|
||||
from {
|
||||
transform: translateY(300px);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bio__contacts {
|
||||
animation: fadeInAnimation ease-out 2000ms;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
|
||||
@keyframes fadeInAnimation {
|
||||
0% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: $white2;
|
||||
color: $black;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: $fw-4;
|
||||
text-align: start;
|
||||
text-rendering: optimizeLegibility;
|
||||
transition: background 0.2s linear;
|
||||
a {
|
||||
color: $black;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
background: $black2;
|
||||
color: $color-light;
|
||||
|
||||
#logo {
|
||||
a {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-green;
|
||||
}
|
||||
|
||||
.profile__card {
|
||||
.card__img {
|
||||
#headshot {
|
||||
border: 3px solid $grey3;
|
||||
}
|
||||
}
|
||||
}
|
||||
.project {
|
||||
background: $black;
|
||||
color: $white;
|
||||
box-shadow: 0 7px 30px -10px rgba(150, 170, 180, 0.2);
|
||||
|
||||
.project__title a {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.project__info {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
@include respond-above(sm) {
|
||||
.project__title a {
|
||||
color: $grey3;
|
||||
}
|
||||
|
||||
.project__info {
|
||||
color: $grey3;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.project__bar {
|
||||
background-color: $grey4;
|
||||
}
|
||||
.project__title a {
|
||||
color: $white;
|
||||
}
|
||||
.project__info {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
.project__tagbox {
|
||||
.tag__item {
|
||||
background: $grey4;
|
||||
:hover {
|
||||
background: $grey2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.App {
|
||||
margin: 0 auto;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
@include respond-above(xs) {
|
||||
// margin: 0 1rem;
|
||||
max-width: 990px;
|
||||
}
|
||||
}
|
||||
|
||||
.app__wrapper {
|
||||
margin: auto 1rem;
|
||||
}
|
||||
|
||||
header {
|
||||
line-height: 1.7;
|
||||
// margin: 0 1rem;
|
||||
padding-top: 30px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
// .wrapper_main {
|
||||
// // margin: 0 1rem;
|
||||
// // max-width: 768px;
|
||||
// }
|
||||
@@ -1,424 +0,0 @@
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
#logo {
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-7;
|
||||
letter-spacing: 0.2rem;
|
||||
margin-left: 8px;
|
||||
a {
|
||||
color: $grey2;
|
||||
}
|
||||
a:hover {
|
||||
color: $black2;
|
||||
}
|
||||
}
|
||||
#nav__list {
|
||||
text-align: end;
|
||||
font-weight: $fw-5;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.02rem;
|
||||
}
|
||||
a {
|
||||
color: $black2;
|
||||
}
|
||||
a:hover {
|
||||
background-color: $green;
|
||||
color: $black;
|
||||
transition: $transition-hl;
|
||||
}
|
||||
}
|
||||
|
||||
.switch-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.switch-wrap {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 25px;
|
||||
cursor: pointer;
|
||||
background: $grey;
|
||||
padding: $sw-padding;
|
||||
width: $sw-width;
|
||||
height: $sw-height;
|
||||
border-radius: $sw-height / 2;
|
||||
input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.switch {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 0fr 1fr 1fr;
|
||||
transition: 0.2s;
|
||||
//ICYMI, pseudo elements are treated as grid items
|
||||
&::after {
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
background: #ccc;
|
||||
grid-column: 2;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked {
|
||||
+ .switch {
|
||||
grid-template-columns: 1fr 1fr 0fr;
|
||||
&::after {
|
||||
background-color: $green2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#profile {
|
||||
// min-height: 80vh;
|
||||
h2 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.profile__card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
.card__img {
|
||||
margin-bottom: 8px;
|
||||
|
||||
#headshot {
|
||||
height: 125px;
|
||||
width: 125px;
|
||||
border: 3px solid $black2;
|
||||
border-radius: 50%;
|
||||
box-shadow: $shadow-360;
|
||||
}
|
||||
}
|
||||
|
||||
.card__bio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: padding-top 800ms ease;
|
||||
|
||||
.bio__name {
|
||||
line-height: 0.9em;
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-7;
|
||||
|
||||
a {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bio__desc {
|
||||
margin: 8px auto 14px;
|
||||
font-weight: $fw-6;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bio__contacts {
|
||||
width: 125px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
a {
|
||||
color: $grey4;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
color: $grey2;
|
||||
transition: $transition-hl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include respond-above(xs) {
|
||||
padding-top: 36px;
|
||||
}
|
||||
@include respond-below(xs) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile__about {
|
||||
margin: 2em auto;
|
||||
line-height: 1.8em;
|
||||
max-width: 764px;
|
||||
|
||||
@include respond-above(xs) {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
@include respond-above(sm) {
|
||||
// margin: 2em 3rem;
|
||||
}
|
||||
transition: padding-top 800ms ease;
|
||||
}
|
||||
|
||||
.profile__skills {
|
||||
ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
li {
|
||||
border: 1px solid $color-grey;
|
||||
border-radius: 2px;
|
||||
padding: 4px 12px;
|
||||
margin: 5px 3px;
|
||||
}
|
||||
|
||||
@include respond-above(xs) {
|
||||
margin: 0 auto;
|
||||
max-width: 613px;
|
||||
}
|
||||
}
|
||||
|
||||
#projects {
|
||||
padding: 70px 0 60px;
|
||||
|
||||
h2 {
|
||||
display: none;
|
||||
color: $color-grey;
|
||||
font-size: $fs-xl;
|
||||
}
|
||||
|
||||
// @include respond-above(sm) {
|
||||
// max-width: 990px;
|
||||
// margin: 3rem auto;
|
||||
// }
|
||||
}
|
||||
|
||||
.project {
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: $shadow-360;
|
||||
border-radius: 10px;
|
||||
margin: 3rem auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
// max-width: 800px;
|
||||
|
||||
a,
|
||||
a:hover {
|
||||
color: $color-dark;
|
||||
}
|
||||
|
||||
h3,
|
||||
.h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: $weight-heavy;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.project__subtitle_small {
|
||||
font-size: 80%;
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.project__title {
|
||||
font-size: 1.75rem;
|
||||
a {
|
||||
transition: color 0.1s ease;
|
||||
transition: background 250ms ease-in-out;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background: $color-green;
|
||||
}
|
||||
}
|
||||
.project__img {
|
||||
max-height: 180px;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.project__img_link {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.project__bar {
|
||||
width: 50px;
|
||||
height: 8px;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
background-color: $color-lgrey;
|
||||
transition: background-color 0.1s ease;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.project__text {
|
||||
padding: 1.5rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// background: $color-light;
|
||||
}
|
||||
|
||||
.project__info {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: justify;
|
||||
height: 100%;
|
||||
color: $color-dark;
|
||||
transition: color 0.1s ease;
|
||||
}
|
||||
|
||||
.project__tagbox {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
font-size: 14px;
|
||||
margin: 20px 0 0 0;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
|
||||
.tag__item {
|
||||
display: inline-block;
|
||||
background: $color-lgrey;
|
||||
color: $color-dark;
|
||||
border-radius: 3px;
|
||||
padding: 5px 10px;
|
||||
margin: 0 5px 5px 0;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
transition: background 0.1s ease-in-out;
|
||||
transition: color 0.05s ease-out;
|
||||
|
||||
&:hover {
|
||||
background: $color-grey;
|
||||
color: $color-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.project__bar {
|
||||
width: 100px;
|
||||
background-color: $color-grey;
|
||||
}
|
||||
|
||||
.project__title {
|
||||
a {
|
||||
color: $color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.project__info {
|
||||
color: $color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
@include respond-above(sm) {
|
||||
flex-wrap: inherit;
|
||||
|
||||
a {
|
||||
color: $color-grey;
|
||||
}
|
||||
|
||||
.project__title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.project__tagbox {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.project__img {
|
||||
max-width: 250px;
|
||||
max-height: 100%;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.project__text {
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.project__info {
|
||||
color: $color-grey;
|
||||
}
|
||||
|
||||
.media.project__text:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: -20%;
|
||||
height: 130%;
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
&:hover .project__img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&:nth-child(2n + 0) {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) .project__text::before {
|
||||
left: -12px !important;
|
||||
transform: rotate(4deg);
|
||||
}
|
||||
|
||||
&:nth-child(2n + 0) .project__text::before {
|
||||
right: -12px !important;
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
// height: 200px;
|
||||
max-width: 500px;
|
||||
.foot__links {
|
||||
// border: 1px solid yellow;
|
||||
font-size: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin-bottom: 50px;
|
||||
a {
|
||||
color: $green2;
|
||||
|
||||
:hover {
|
||||
color: $green3;
|
||||
}
|
||||
}
|
||||
}
|
||||
.copyright {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
/* No CSS *//*# sourceMappingURL=mixins.css.map */
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": "",
|
||||
"sources": [
|
||||
"mixins.scss"
|
||||
],
|
||||
"names": [],
|
||||
"file": "mixins.css"
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// A map of breakpoints.
|
||||
$breakpoints: (
|
||||
xs: 576px,
|
||||
sm: 768px,
|
||||
md: 992px,
|
||||
lg: 1200px,
|
||||
);
|
||||
|
||||
// RESPOND ABOVE
|
||||
//––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
// @include respond-above(sm) {}
|
||||
@mixin respond-above($breakpoint) {
|
||||
// If the breakpoint exists in the map.
|
||||
@if map-has-key($breakpoints, $breakpoint) {
|
||||
// Get the breakpoint value.
|
||||
$breakpoint-value: map-get($breakpoints, $breakpoint);
|
||||
// Write the media query.
|
||||
@media (min-width: $breakpoint-value) {
|
||||
@content;
|
||||
}
|
||||
// If the breakpoint doesn't exist in the map.
|
||||
} @else {
|
||||
// Log a warning.
|
||||
@warn 'Invalid breakpoint: #{$breakpoint}.';
|
||||
}
|
||||
}
|
||||
|
||||
// RESPOND BELOW
|
||||
// ––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
// @include respond-below(sm) {}
|
||||
@mixin respond-below($breakpoint) {
|
||||
@if map-has-key($breakpoints, $breakpoint) {
|
||||
$breakpoint-value: map-get($breakpoints, $breakpoint);
|
||||
|
||||
@media (max-width: ($breakpoint-value - 1)) {
|
||||
@content;
|
||||
}
|
||||
} @else {
|
||||
@warn 'Invalid breakpoint: #{$breakpoint}.';
|
||||
}
|
||||
}
|
||||
|
||||
// RESPOND BETWEEN
|
||||
//––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
// @include respond-between(sm, md) {}
|
||||
@mixin respond-between($lower, $upper) {
|
||||
// If both the lower and upper breakpoints exist in the map.
|
||||
@if map-has-key($breakpoints, $lower) and map-has-key($breakpoints, $upper)
|
||||
{
|
||||
// Get the lower and upper breakpoints.
|
||||
$lower-breakpoint: map-get($breakpoints, $lower);
|
||||
$upper-breakpoint: map-get($breakpoints, $upper);
|
||||
|
||||
@media (min-width: $lower-breakpoint) and (max-width: ($upper-breakpoint - 1)) {
|
||||
@content;
|
||||
}
|
||||
} @else {
|
||||
@if (map-has-key($breakpoints, $lower) == false) {
|
||||
@warn 'Your lower breakpoint was invalid: #{$lower}.';
|
||||
}
|
||||
|
||||
@if (map-has-key($breakpoints, $upper) == false) {
|
||||
@warn 'Your upper breakpoint was invalid: #{$upper}.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/* No CSS *//*# sourceMappingURL=variables.css.map */
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"mappings": "",
|
||||
"sources": [
|
||||
"variables.scss"
|
||||
],
|
||||
"names": [],
|
||||
"file": "variables.css"
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
$background-dark: #272727;
|
||||
$background-light: #f7f9fb;
|
||||
|
||||
$color-dark: #272727;
|
||||
$color-light: #fafafa;
|
||||
|
||||
$color-dark2: #1b1b1b;
|
||||
|
||||
$color-grey: #707171;
|
||||
|
||||
$color-lgrey: #c9cacc;
|
||||
$color-blue: #61dafb;
|
||||
$color-green: #2bf3c4;
|
||||
|
||||
$green: #2bf3c4;
|
||||
$green2: #27bb98;
|
||||
$green3: #238770;
|
||||
$green4: #1f4b40;
|
||||
|
||||
$black: #1b1b1b;
|
||||
$black2: #272727;
|
||||
|
||||
$grey: #4b4b4b;
|
||||
$grey2: #707171;
|
||||
$grey3: #999a9a;
|
||||
$grey4: #c5c6c6;
|
||||
|
||||
$white: #fafafa;
|
||||
$white2: #f7f9fb;
|
||||
|
||||
$fs-xs: 0.5rem;
|
||||
$fs-sm: 0.8rem;
|
||||
$fs-md: 1rem;
|
||||
$fs-lg: 1.8rem;
|
||||
$fs-xl: 2.3rem;
|
||||
|
||||
$fw-2: 200;
|
||||
$fw-3: 300;
|
||||
$fw-4: 400;
|
||||
$fw-5: 500;
|
||||
$fw-6: 600;
|
||||
$fw-7: 700;
|
||||
$fw-8: 800;
|
||||
|
||||
$weight-norm: 400;
|
||||
$weight-mid: 500;
|
||||
$weight-heavy: 700;
|
||||
|
||||
$transition-hl: 150ms ease;
|
||||
|
||||
$shadow-360: 0 7px 30px -10px rgba(150, 170, 180, 0.5);
|
||||
$shadow-360-og: 0 10px 25px 5px rgba(0, 0, 0, 0.2);
|
||||
$shadow-360-inset: inset -10px -20px 20px 1px rgba(0, 0, 0, 0.2);
|
||||
|
||||
$sw-width: 50px;
|
||||
$sw-padding: 6px;
|
||||
$sw-height: $sw-width / 2 + $sw-padding;
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||