feat: new website

This commit is contained in:
Dan Finch 2026-05-13 02:03:11 +02:00
commit 63a654aab4
64 changed files with 2904 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
node_modules
dist
*.tgz
.env
.env.*
.cache
*.tsbuildinfo

7
LICENSE Normal file
View file

@ -0,0 +1,7 @@
Copyright © 2026 Dan Finch
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# da.nfin.ch
My website.
Built with [hypeup](https://github.com/sigitex/hypeup) SSG.

258
assets/background.js Normal file
View file

@ -0,0 +1,258 @@
// 100% VIBES
const canvas = document.getElementById('background');
const gl = canvas.getContext('webgl');
if (gl) {
function resize() {
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
gl.getExtension('OES_standard_derivatives');
const vsrc = `
attribute vec2 a_pos;
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
`;
const fsrc = `
#extension GL_OES_standard_derivatives : enable
precision highp float;
uniform float u_time;
uniform float u_glitch;
uniform vec2 u_res;
#define COL_SKY_TOP vec3(0.04, 0.0, 0.12)
#define COL_SKY_MID vec3(0.18, 0.02, 0.28)
#define COL_SKY_LOW vec3(0.85, 0.25, 0.45)
#define COL_HORIZON vec3(1.0, 0.55, 0.2)
#define COL_GRID vec3(0.0, 0.95, 1.0)
#define COL_GRID2 vec3(0.72, 0.4, 1.0)
#define COL_MOUNT vec3(0.06, 0.0, 0.14)
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float mountain(float x, float scale, float height, float offset) {
float m = 0.0;
m += abs(fract(x * 0.4 + offset * 0.1) - 0.5) * 2.0 * 0.5;
m += abs(fract(x * 0.85 + offset * 0.17) - 0.5) * 2.0 * 0.3;
m += abs(fract(x * 1.9 + offset * 0.06) - 0.5) * 2.0 * 0.15;
float cx = abs(x) / (scale * 0.1 + 0.5);
float sideMask = smoothstep(0.0, 0.5, cx);
return m * height * sideMask;
}
float glitch(float y, float t, float g) {
if (g < 0.01) return 0.0;
float band = step(0.7, hash(vec2(floor(y * 30.0), floor(t * 20.0))));
float off = (hash(vec2(floor(y * 30.0), floor(t * 30.0))) - 0.5) * 0.06;
return off * band * g;
}
float glitchColor(float y, float t, float g) {
float band = step(0.6, hash(vec2(floor(y * 25.0), floor(t * 15.0))));
return band * g;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_res;
float aspect = u_res.x / u_res.y;
float t = u_time;
float gOff = glitch(uv.y, t, u_glitch);
uv.x += gOff;
vec2 cuv = (uv - 0.5) * vec2(aspect, 1.0);
float horizon = 0.42;
vec3 col = vec3(0.0);
if (uv.y > horizon) {
float skyT = (uv.y - horizon) / (1.0 - horizon);
vec3 sky = vec3(0.0);
if (skyT < 0.15) {
sky = mix(COL_HORIZON, COL_SKY_LOW, skyT / 0.15);
} else if (skyT < 0.5) {
sky = mix(COL_SKY_LOW, COL_SKY_MID, (skyT - 0.15) / 0.35);
} else {
sky = mix(COL_SKY_MID, COL_SKY_TOP, (skyT - 0.5) / 0.5);
}
vec2 sunPos = vec2(-0.3, -0.02);
float sunDist = length(cuv - sunPos);
float sunR = 0.35;
if (sunDist < sunR) {
float sunT = (cuv.y - sunPos.y + sunR) / (2.0 * sunR);
vec3 sunCol = mix(vec3(0.8, 0.05, 0.1), vec3(1.0, 0.85, 0.3), sunT);
float sunEdge = smoothstep(sunR, sunR - 0.006, sunDist);
sky = mix(sky, sunCol, sunEdge);
}
float glow = exp(-sunDist * 2.5) * 0.7;
sky += vec3(1.0, 0.25, 0.15) * glow;
float mx = cuv.x;
float m1 = mountain(mx * 2.5, 8.0, 1.0, 0.0) * 0.45;
float mH1 = (uv.y - horizon) - m1 * (1.0 - horizon);
float fw1 = fwidth(mH1);
float mMask1 = smoothstep(-fw1, fw1, mH1);
sky = mix(sky, COL_MOUNT, 1.0 - mMask1);
float m2 = mountain(mx * 3.5, 12.0, 1.0, 5.0) * 0.35;
float mH2 = (uv.y - horizon) - m2 * (1.0 - horizon);
float fw2 = fwidth(mH2);
float mMask2 = smoothstep(-fw2, fw2, mH2);
sky = mix(sky, COL_MOUNT * 0.7, 1.0 - mMask2);
col = sky;
}
if (uv.y <= horizon) {
float floorT = (horizon - uv.y) / horizon;
float z = 0.3 / (floorT + 0.01);
float x = cuv.x * z * 2.5;
float scrollSpeed = 0.2;
z += t * scrollSpeed;
float gx = x * 1.8;
float gz = z * 0.6;
float lineX = abs(fract(gx) - 0.5);
float lineZ = abs(fract(gz) - 0.5);
float fwX = fwidth(gx) * 1.0;
float fwZ = fwidth(gz) * 1.0;
float gridX = smoothstep(fwX, 0.0, lineX) * 0.7;
float gridZ = smoothstep(fwZ, 0.0, lineZ) * 0.7;
float grid = max(gridX, gridZ);
float fadeDist = smoothstep(0.85, 0.0, floorT);
float fadeNear = smoothstep(0.0, 0.05, floorT);
grid *= fadeDist * fadeNear;
vec3 gridCol = mix(COL_GRID, COL_GRID2, floorT);
vec3 ground = vec3(0.02, 0.0, 0.05);
float hGlow = exp(-floorT * 6.0) * 0.3;
ground += COL_HORIZON * hGlow;
col = ground + gridCol * grid;
}
float hLine = exp(-abs(uv.y - horizon) * 120.0) * 0.5;
col += vec3(1.0, 0.5, 0.3) * hLine;
float scanline = sin(gl_FragCoord.y * 1.5) * 0.04 + 0.96;
col *= scanline;
vec2 vc = uv * 2.0 - 1.0;
float vignette = 1.0 - dot(vc * 0.5, vc * 0.5);
col *= smoothstep(0.0, 0.7, vignette);
float gc = glitchColor(uv.y, t, u_glitch);
col.r += gc * 0.15;
col.b -= gc * 0.1;
col = pow(col, vec3(0.95));
gl_FragColor = vec4(col, 1.0);
}
`;
function compile(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(s));
return null;
}
return s;
}
const vs = compile(gl.VERTEX_SHADER, vsrc);
const fs = compile(gl.FRAGMENT_SHADER, fsrc);
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(prog));
}
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, 'a_pos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
const uTime = gl.getUniformLocation(prog, 'u_time');
const uGlitch = gl.getUniformLocation(prog, 'u_glitch');
const uRes = gl.getUniformLocation(prog, 'u_res');
const glitchDuration = 300;
let glitchStart = -glitchDuration;
let elapsed = 0;
let lastFrame = 0;
let raf = null;
document.addEventListener('click', function() {
glitchStart = performance.now();
}, true);
function glitchAmount(now) {
const age = now - glitchStart;
if (age < 0 || age > glitchDuration) return 0;
return Math.sin((age / glitchDuration) * Math.PI);
}
function frame(now) {
if (document.hidden) {
raf = null;
lastFrame = 0;
return;
}
if (lastFrame) {
elapsed += Math.min((now - lastFrame) / 1000, 0.1);
}
lastFrame = now;
gl.uniform1f(uTime, elapsed);
gl.uniform1f(uGlitch, glitchAmount(now));
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
raf = requestAnimationFrame(frame);
}
function startAnimation() {
if (!raf && !document.hidden) {
lastFrame = 0;
raf = requestAnimationFrame(frame);
}
}
document.addEventListener('visibilitychange', startAnimation);
startAnimation();
}

1310
bun.lock Normal file

File diff suppressed because it is too large Load diff

2
bunfig.toml Normal file
View file

@ -0,0 +1,2 @@
[test]
pathIgnorePatterns = ["dist/**"]

6
compose.yml Normal file
View file

@ -0,0 +1,6 @@
name: da.nfin.ch
services:
printer:
image: gotenberg/gotenberg:8
ports:
- "3000:3000"

BIN
cv.pdf Normal file

Binary file not shown.

26
da.nfin.ch.code-workspace Normal file
View file

@ -0,0 +1,26 @@
{
"folders": [
{
"name": "da.nfin.ch",
"path": ".",
},
],
"settings": {
"oxc.path.oxfmt": "node_modules/.bin/oxfmt",
"oxc.path.oxlint": "node_modules/.bin/oxlint",
"files.exclude": {
"**/.git": true,
"**/node_modules": true,
"**/.temp": true,
"**/dist": true,
"**/*.tsbuildinfo": true,
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"tsconfig.json": "tsconfig.*.json",
"package.json": "bun.lock, bunfig.toml, *.bun.plugin.ts, ox*.config.ts, .gitignore, .mirrorignore, regime.*.json, regime.internal.json, commitlint.config.*, release.config.*js, hypeup.config.*",
"vite.config.ts": "*.vite.plugin.ts",
"README.md": "LICENSE, LICENSE.md",
},
},
}

BIN
errilaz.org/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
errilaz.org/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

137
errilaz.org/index.html Normal file
View file

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>errilaz</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/png" href="/favicon.png">
<style>/*! minireset.css v0.0.2 | MIT License | github.com/jgthms/minireset.css */html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*{box-sizing:inherit}*:before,*:after{box-sizing:inherit}img,embed,object,audio,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,td{padding:0;text-align:left}</style>
<style>
body {
background-color: black;
color: silver;
font-family: sans-serif;
font-size: 20px;
}
main {
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
h1 {
color: #ff0079;
font-size: 42px;
font-weight: bold;
letter-spacing: -2px;
margin-top: 20px;
margin-bottom: 20px;
text-align: center;
}
blockquote {
text-align: center;
color: #606060;
font-style: italic;
font-size: 16px;
}
blockquote a {
color: #ffffff;
}
blockquote a:hover {
background-color: white;
color: black;
}
h2 {
font-size: 22px;
margin-top: 30px;
margin-bottom: 20px;
}
a {
color: #ff0079;
text-decoration: none;
}
a:hover {
background-color: #ff0079;
color: #000000;
}
svg {
width: 20px;
height: 20px;
margin-bottom: -5px;
}
table {
width: 100%;
}
tr {
border-bottom: solid 1px #3b011d;
}
td {
padding: 20px;
}
td:last-child {
text-align: right;
font-size: 16px;
}
footer {
margin-top: 30px;
font-size: 12px;
color: #606060;
text-align: center;
}
code {
background-color: #101010;
color: #a0a0a0;
font-size: 14px
}
.links {
margin-top: 20px;
display: flex;
flex-direction: row;
justify-content:space-between;
gap: 10px;
}
.links li {
display: flex;
width: 80px;
height: 80px;
background-color: #101010;
}
.links li a {
font-size: 14px;
display: flex;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
text-align: center;
}
</style>
</head>
<body>
<main>
<h1>errilaz</h1>
<blockquote>
software thaumaturgy<br>
</blockquote>
<ul class="links">
<li>
<a href="https://da.nfin.ch/">personal site</a>
</li>
<li>
<a href="https://sigitex.com/">projects</a>
</li>
<li>
<a href="https://github.com/errilaz">github</a>
</li>
<li>
<a href="https://www.npmjs.com/~errilaz">npm</a>
</li>
<li>
<a href="https://hub.docker.com/u/errilaz">docker</a>
</li>
</ul>
<footer>
&copy; Dan Finch
</footer>
</main>
</body>
</html>

12
hypeup.config.ts Normal file
View file

@ -0,0 +1,12 @@
import { defineConfig } from "hypeup"
export default defineConfig({
clean: true,
dir: "src/routes",
vite: {
server: {
allowedHosts: [Bun.env.HOSTNAME!],
host: true,
},
},
})

24
oxfmt.config.ts Normal file
View file

@ -0,0 +1,24 @@
import { defineConfig } from "oxfmt"
export default defineConfig({
useTabs: false,
tabWidth: 2,
printWidth: 80,
singleQuote: false,
jsxSingleQuote: false,
quoteProps: "as-needed",
trailingComma: "all",
semi: false,
arrowParens: "always",
bracketSameLine: false,
bracketSpacing: true,
ignorePatterns: ["**/*.gen.ts"],
overrides: [
{
files: ["**/ui/**/*.ts"],
options: {
printWidth: 60,
},
},
],
})

93
oxlint.config.ts Normal file
View file

@ -0,0 +1,93 @@
import { defineConfig } from "oxlint"
export default defineConfig({
plugins: ["typescript", "unicorn", "oxc"],
ignorePatterns: ["**/*.gen.ts", "node_modules/**/*"],
categories: {
correctness: "error",
suspicious: "warn",
perf: "warn",
style: "warn",
restriction: "error",
},
rules: {
"capitalized-comments": "off",
"default-case": "off",
"filename-case": "off",
"func-style": ["error", "declaration", { allowArrowFunctions: true }],
"id-length": "off",
"init-declarations": "off",
"max-params": "off",
"max-statements": "off",
"new-cap": "off",
"no-array-for-each": "off",
"no-async-await": "off",
"no-await-expression-member": "off",
"no-await-in-loop": "off",
"no-bitwise": "off",
"no-console": "off",
"no-continue": "off",
"no-dynamic-delete": "off",
"no-empty-file": "off",
"no-empty-function": "off",
"no-eq-null": "warn",
"no-implicit-coercion": "off",
"no-magic-numbers": "off",
"no-multi-assign": "off",
"no-nested-ternary": "off",
"unicorn/no-nested-ternary": "off",
"no-null": "off",
"no-optional-chaining": "off",
"no-plusplus": "off",
"no-rest-spread-properties": "off",
"no-shadow-restricted-names": "off",
"no-shadow": "off",
"no-ternary": "off",
"no-undefined": "off",
"no-underscore-dangle": "off",
"no-use-before-define": "off",
"unicorn/numeric-separators-style": "off",
"prefer-destructuring": "off",
"prefer-for-of": "off",
"prefer-template": "off",
"prefer-ternary": "off",
"require-module-specifiers": "off",
"sort-imports": "off",
"sort-keys": "off",
"switch-case-braces": "off",
"typescript/consistent-indexed-object-style": "off",
"typescript/consistent-type-definitions": ["error", "type"],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-member-accessibility": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-interface": "off",
"typescript/no-empty-object-type": "off",
"typescript/no-namespace": "off",
"typescript/no-non-null-assertion": "off",
"typescript/prefer-function-type": "off",
"unicorn/no-process-exit": "off",
"unicorn/prefer-string-raw": "off",
"unicorn/text-encoding-identifier-case": "off",
},
overrides: [
{
files: ["*.test.ts"],
rules: {
"typescript/no-explicit-any": "off",
"typescript/no-require-imports": "off",
"typescript/no-var-requires": "off",
"unicorn/prefer-module": "off",
"unicorn/consistent-function-scoping": "off",
"func-names": "off",
},
},
{
files: ["*.d.ts"],
rules: {
"typescript/no-explicit-any": "off",
"unicorn/consistent-function-scoping": "off",
"typescript/consistent-type-definitions": "off",
},
},
],
})

51
package.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "da.nfin.ch",
"private": true,
"type": "module",
"license": "MIT",
"author": {
"name": "Sigitex",
"url": "http://github.com/sigitex"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sigitex/da.nfin.ch.git"
},
"files": [
"src"
],
"scripts": {
"check": "tsgo --build",
"test": "bun test --pass-with-no-tests --tsconfig-override tsconfig.test.json",
"lint": "oxlint",
"start": "hypeup generate --watch",
"build": "bun run build:site && bun run build:cv",
"build:site": "hypeup generate",
"build:cv": "bun scripts/build-cv.ts",
"publish:site": "rsync -avz --delete dist/ da.nfin.ch:~/da.nfin.ch",
"publish:errilaz": "rsync -avz --delete errilaz.org/ da.nfin.ch:~/errilaz.org",
"publish": "bun run publish:site && bun run publish:errilaz"
},
"dependencies": {
"98se.css": "/err/98se.css",
"@fontsource/roboto-condensed": "^5.2.8",
"@hypeup/client": "workspace:*",
"@hypeup/lexicon": "workspace:*",
"@hypeup/runtime": "workspace:*",
"hypeup": "workspace:*",
"marked": "^18.0.3",
"marked-gfm-heading-id": "^4.1.4"
},
"devDependencies": {
"@types/bun": "^1.3.13",
"@typescript/native-preview": "beta",
"oxfmt": "^0.47.0",
"oxlint": "^1.62.0",
"playwright": "^1.54.1"
},
"workspaces": {
"packages": [
"/sig/hypeup/packages/*"
]
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/icons/about.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

BIN
public/icons/contact.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
public/icons/experience.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

4
public/icons/github.svg Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.35003 16.88C9.35003 16.95 9.28003 17 9.18003 17C9.08003 17 9.00003 17 9.00003 16.88C9.00003 16.76 9.08003 16.76 9.17003 16.76C9.26003 16.76 9.35003 16.81 9.35003 16.88ZM8.35003 16.73C8.35003 16.8 8.35003 16.88 8.49003 16.9C8.52584 16.9172 8.56701 16.9195 8.6045 16.9064C8.642 16.8933 8.67275 16.8658 8.69003 16.83C8.69003 16.76 8.69003 16.69 8.55003 16.66C8.41003 16.63 8.37003 16.66 8.35003 16.73ZM9.77003 16.68C9.68003 16.68 9.62003 16.76 9.63003 16.84C9.64003 16.92 9.72003 16.95 9.82003 16.93C9.92003 16.91 9.97003 16.84 9.96003 16.77C9.95003 16.7 9.87003 16.67 9.77003 16.68ZM11.9 4.00002C10.8454 3.99009 9.79962 4.19333 8.82547 4.59754C7.85132 5.00175 6.96887 5.5986 6.23107 6.35227C5.49328 7.10594 4.91535 8.0009 4.53197 8.98343C4.14859 9.96597 3.96765 11.0158 4.00003 12.07C3.97211 13.7969 4.48426 15.4894 5.46493 16.9111C6.4456 18.3328 7.84582 19.4127 9.47003 20C9.88003 20.07 10.03 19.81 10.03 19.6C10.03 19.39 10.03 18.26 10.03 17.6C10.03 17.6 7.77003 18.1 7.29003 16.6C7.29003 16.6 6.93003 15.6 6.40003 15.39C6.40003 15.39 5.66003 14.87 6.45003 14.88C6.70877 14.9149 6.95573 15.01 7.17108 15.1576C7.38643 15.3052 7.56417 15.5013 7.69003 15.73C7.79466 15.9351 7.9401 16.1167 8.11742 16.2635C8.29473 16.4104 8.50019 16.5195 8.72118 16.5841C8.94218 16.6487 9.17404 16.6675 9.40255 16.6393C9.63106 16.6111 9.85139 16.5364 10.05 16.42C10.0879 16.0025 10.2679 15.6107 10.56 15.31C8.76003 15.1 6.94003 14.84 6.94003 11.65C6.92091 11.2896 6.97881 10.9293 7.10985 10.5931C7.2409 10.2569 7.44209 9.95241 7.70003 9.70002C7.45667 8.96799 7.48507 8.17282 7.78003 7.46002C8.46003 7.24002 10.01 8.35002 10.01 8.35002C11.3342 7.97655 12.7359 7.97655 14.06 8.35002C14.06 8.35002 15.61 7.24002 16.29 7.46002C16.5914 8.17142 16.6198 8.96894 16.37 9.70002C16.6371 9.94893 16.8489 10.2511 16.9919 10.587C17.1348 10.9229 17.2057 11.285 17.2 11.65C17.2 14.85 15.3 15.1 13.5 15.31C13.6809 15.5129 13.8186 15.7506 13.9046 16.0085C13.9905 16.2664 14.023 16.5391 14 16.81C14 17.93 14 19.31 14 19.58C13.9994 19.6475 14.015 19.7142 14.0456 19.7744C14.0763 19.8346 14.1209 19.8866 14.1759 19.9258C14.2308 19.9651 14.2945 19.9905 14.3613 19.9999C14.4282 20.0094 14.4964 20.0025 14.56 19.98C16.1813 19.3978 17.5786 18.321 18.5547 16.9017C19.5309 15.4824 20.0364 13.7922 20 12.07C20.0094 11.0051 19.8061 9.94902 19.402 8.96371C18.9979 7.9784 18.4011 7.08369 17.6467 6.33205C16.8923 5.58041 15.9953 4.98696 15.0085 4.58651C14.0217 4.18606 12.9649 3.98667 11.9 4.00002ZM7.14003 15.41C7.14003 15.41 7.14003 15.52 7.14003 15.58C7.15118 15.5912 7.16442 15.6001 7.17901 15.6061C7.1936 15.6122 7.20923 15.6153 7.22503 15.6153C7.24082 15.6153 7.25646 15.6122 7.27105 15.6061C7.28563 15.6001 7.29888 15.5912 7.31003 15.58C7.31003 15.58 7.31003 15.47 7.31003 15.4C7.31003 15.33 7.18003 15.37 7.14003 15.41ZM6.79003 15.14C6.79003 15.14 6.79003 15.24 6.86003 15.27C6.86846 15.2805 6.87913 15.2889 6.89124 15.2947C6.90335 15.3004 6.91661 15.3035 6.93003 15.3035C6.94345 15.3035 6.9567 15.3004 6.96881 15.2947C6.98093 15.2889 6.99159 15.2805 7.00003 15.27C7.00003 15.27 7.00003 15.17 6.93003 15.14C6.86003 15.11 6.81003 15.11 6.79003 15.14ZM7.79003 16.32C7.79003 16.32 7.79003 16.46 7.79003 16.53C7.79003 16.6 7.96003 16.61 8.00003 16.53C8.04003 16.45 8.00003 16.39 8.00003 16.32C8.00003 16.25 7.87003 16.27 7.83003 16.32H7.79003ZM7.42003 15.83C7.42003 15.83 7.42003 15.95 7.42003 16.03C7.42003 16.11 7.56003 16.14 7.61003 16.11C7.63535 16.0809 7.6493 16.0436 7.6493 16.005C7.6493 15.9664 7.63535 15.9291 7.61003 15.9C7.56003 15.82 7.48003 15.79 7.42003 15.83Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 72 72" width="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M8,72 L64,72 C68.418278,72 72,68.418278 72,64 L72,8 C72,3.581722 68.418278,-8.11624501e-16 64,0 L8,0 C3.581722,8.11624501e-16 -5.41083001e-16,3.581722 0,8 L0,64 C5.41083001e-16,68.418278 3.581722,72 8,72 Z" fill="#007EBB"/><path d="M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z" fill="#FFF"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/icons/projects.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

BIN
public/icons/tech.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

BIN
public/pfp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

10
regime.config.json Normal file
View file

@ -0,0 +1,10 @@
{
"templates": [
"profile/workspace",
"profile/app/hypeup-static",
"!include/license"
],
"vars": {
"repo": "da.nfin.ch"
}
}

42
scripts/build-cv.ts Normal file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env bun
import { mkdir } from "node:fs/promises"
import { dirname } from "node:path"
import { chromium } from "playwright"
const url = "file:///finch/da.nfin.ch/dist/cv.html"
const output = "dist/cv.pdf"
await mkdir(dirname(output), { recursive: true })
const executablePath =
process.env.CHROMIUM_PATH ??
Bun.which("thorium-browser") ??
Bun.which("chromium") ??
Bun.which("chromium-browser") ??
Bun.which("google-chrome") ??
Bun.which("google-chrome-canary") ??
undefined
const browser = await chromium.launch({
executablePath,
headless: true,
args: ["--no-sandbox", "--disable-gpu"],
})
try {
const page = await browser.newPage()
await page.goto(url, {
waitUntil: "networkidle",
timeout: 30_000,
})
await page.pdf({
path: output,
printBackground: true,
displayHeaderFooter: false,
preferCSSPageSize: true,
})
} finally {
await browser.close()
}

1
src/ambient.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import "@hypeup/lexicon"

1
src/routes/cv.html.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "ui/CV"

View file

@ -0,0 +1 @@
export { default } from "ui/Experience"

1
src/routes/index.html.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "ui/Home"

View file

@ -0,0 +1 @@
export { default } from "ui/Projects"

1
src/routes/tech.html.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "ui/Tech"

4
src/types.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare type Content = any
declare module "*?raw" { const src: string; export default src }
declare module "*?url" { const src: string; export default src }

192
src/ui/CV/CV.ts Normal file
View file

@ -0,0 +1,192 @@
import Reset from "ui/Layout/Reset"
import Markdown from "ui/shared/Markdown"
import intro from "ui/Home/intro.md?raw"
import details from "ui/Home/details.md?raw"
import skills from "ui/Home/skills.md?raw"
import experience from "ui/Experience/experience.md?raw"
import * as Tech from "ui/Tech/Sections"
import roboto from "@fontsource/roboto-condensed?url"
export default function CV() {
return [
doctype.html5,
html(
head(
link({ rel: "stylesheet", href: roboto }),
Theme(),
),
body(
main(
article(
Header(),
About(),
Skills(),
Experience(),
Expertise(),
),
),
a.pdf.noprint({ href: "cv.pdf" }, "PDF"),
),
),
]
}
function Header() {
return header(
h1("Dan Finch"),
div.links(
div.www(
a({ href: "https://da.nfin.ch" }, "Website"),
a(
{ href: "https://www.sigitex.com" },
"Open Source",
),
a(
{
href: "https://www.linkedin.com/in/danfinch",
},
"LinkedIn",
),
),
div.contact(
a(
{ href: "mailto:danfinch@outlook.com" },
"danfinch@outlook.com",
),
a({ href: "tel:+436645047583" }, "+43 664 5047583"),
),
),
)
}
function About() {
return section.about(
h2("About Me"),
Markdown(intro),
Markdown(details),
)
}
function Skills() {
return section.skills(h2("Skills"), Markdown(skills))
}
function Experience() {
return section.experience(
h2("Experience"),
Markdown(experience),
)
}
function Expertise() {
return section.expertise(
h2("Expertise"),
div.blocks(
Skillset("Core Languages", Tech.languages),
Skillset("Platforms", Tech.platforms),
Skillset("Frontend", Tech.frontend),
Skillset("Backend", Tech.backend),
Skillset("Data", Tech.data),
Skillset("Principles", Tech.principles),
Skillset("Architecture", Tech.architecture),
Skillset("Delivery", Tech.delivery),
Skillset("Quality", Tech.quality),
Skillset("Collaboration", Tech.collaboration),
),
)
}
function Skillset(name: string, content: string) {
return div.block(h3(name), Markdown(content))
}
const Color = {
backdrop: "#f0f0f0",
edge: "#808080",
} as const
function Theme() {
return style(
Reset(),
rule.pdf(
display.flex,
width("50px"),
height("50px"),
borderRadius("12px"),
backgroundColor("#345c42"),
color.white,
position.fixed,
top("25px"),
right("25px"),
justifyContent.center,
alignItems.center,
fontWeight.bold,
textDecoration.none,
),
$page(
prop("size", "auto"),
margin("0 0 0 0"),
padding("0.5in"),
),
rule(body, [
fontFamily("Roboto Condensed"),
fontSize("12pt"),
display.flex,
justifyContent.center,
backgroundColor(Color.backdrop),
]),
$media("print", [
rule(body, backgroundColor.white),
rule.noprint(raw("display: none !important;")),
]),
rule(main, [
backgroundColor.white,
width("8.5in"),
display.flex,
justifyContent.center,
paddingBottom("32pt"),
]),
rule(article, [width("7.0in")]),
rule(header, [
marginTop("0.5in"),
display.flex,
justifyContent.spaceBetween,
alignItems.flexEnd,
paddingBottom("8pt"),
]),
rule(h1, fontSize("28pt")),
rule.links(
fontSize("11pt"),
width("2.75in"),
display.flex,
justifyContent.spaceBetween,
alignItems.center,
rule(">div", [
display.flex,
flexDirection.column,
gap("2pt"),
]),
),
rule.contact(alignItems.flexEnd, fontSize("12pt")),
rule(section, [
borderTop(`double 3px ${Color.edge}`),
paddingTop("16pt"),
paddingBottom("16pt"),
]),
rule(h2, [marginBottom("12pt")]),
rule(p, [marginTop("12pt")]),
rule(ul, [marginTop("12pt")]),
rule(hr, [margin("12pt 0"), borderColor("#f0f0f0")]),
rule(".experience .content h2", [fontSize("14pt")]),
rule.expertise(
pageBreakInside.avoid,
rule(ul, marginBottom("12pt")),
rule.blocks(columns(3)),
rule.block(
flex(1),
minWidth("3in"),
breakInside.avoid,
),
),
)
}

1
src/ui/CV/index.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "./CV"

View file

@ -0,0 +1,26 @@
import Layout from "ui/Layout"
import Window from "ui/Layout/Window"
import Markdown from "ui/shared/Markdown"
import experience from "./experience.md?raw"
const title = "Dan Finch - Experience"
export default function Experience() {
return [
backgroundColor.white,
Layout(
{ title, url: "/experience" },
Window(
{ title },
div(
padding("16px"),
div.content(
h1("My Experience"),
hr(),
Markdown(experience),
),
),
),
),
]
}

View file

@ -0,0 +1,91 @@
## Dynatrace - Senior Software Engineer
> July 2023 - Present (Graz, Austria - Hybrid)
- Implemented an array of features and bug fixes for several Business Analytics applications using **React** and **TypeScript**.
- Designed and implemented a strongly-typed embedded DSL to generate the data warehouse queries which power the apps, significantly reducing maintenance overhead.
- Created an end-to-end testing framework suite based on **Playwright** that substantially streamlined writing and running tests.
- Created and integrated a state management framework based on **Zustand** and **Immer** which greatly simplified and optimized front-end code.
- Initiated ongoing effort to improve code quality and project structure.
- Migrated a large project from Bitbucket+Jenkins to Github+Actions.
---
## Independent Consultant
> Dec 2014 - July 2023 (Remote)
My freelance work in this time included:
- Created a mobile social network app for a startup conference and shared working space provider. This was written in functional **TypeScript** and **React Native**, with some Python/Django and Postgres on a Heroku backend.
- Consulted on JavaScript and front-end tech for Bitbanger Labs for their ColorSpike hybrid mobile app built on Cordova.
- Wrote a Chrome extension using the **Mithril** framework to inspect advertisements embedded in web pages for Bringhub.
- Designed and prototyped an app and backend for a IOT equipment monitoring startup in Sandpoint, ID using **React**, **ElasticSearch**, and **Postgres**.
- Created an **Electron** app connected to the QuickBooks API to build quotes for a greenhouse manufacturer.
---
## RiskLens - Senior Software Engineer
> Jun 2017 - Dec 2017 (Spokane, WA)
Helped lead the front-end development of FAIR-U - a **React** + **Redux**-based app backed by **C\#**, Sql Server, and
Azure & transitioned the team to TypeScript and other modern front-end tools. Gained experience with a
security-focused and process-intensive team.
---
## TenX Logic - Contract Developer
> Oct 2015 - Mar 2016 (Remote)
This project for the oil + gas industry targeted the Mono platform on ARM processors. I designed and
implemented a **C\#**-based driver model to control equipment in the field, with a frontend written in AngularJS.
---
## Kochava - Senior Developer
> Apr 2014 - Nov 2014 (Sandpoint, ID)
Worked on infrastructure and systems to manage large volumes of real-time analytics data. I tackled
complex challenges requiring constant inventiveness and collaboration to help the company scale at
a rapid rate.
My work centered around **Node.js**, **Redis**, Linux, and **MongoDB**. I also led the early implementation of a
scalable high-traffic analytics backend.
---
## Coldwater Creek - Web Developer
> Oct 2012 - Apr 2014 (Sandpoint, ID)
Worked with many teams to finish an amazing number of projects. My work on both internal tooling and
customer experience put the company ahead of its industry in many respects.
- Implemented a product search which outperformed all competitors, including a novel infinite scrolling method.
- Provided an array of features and user interfaces widgets for website with around one million hits per day.
- Wrote or re-wrote around 30 internal web-driven tools for e-commerce & marketing teams.
- Wrote custom jQuery UI powered framework and widgets for tool suite.
- Initiated effort to significantly improve client-side page load performance.
- Created a framework allowing mutual embedding between MVC and WebForms to allow a smooth transition off of legacy code.
---
## Promitheia - Technical Director
> Sep 2006 - Sep 2012 (Austin, TX)
As co-founder of this startup I was responsible for software design, programming, project management,
operations, testing, maintenance, and security.
- Created complex e-commerce website featuring retail and affiliate marketing management. Owner of dozens of projects within the company.
- Configured and managed production web servers including Microsoft IIS and LAMP stack.
- Worked closely with DBA on data-driven applications powered by SQL Server 2008 and ADO.NET.
- Analyzed and selected supporting software and services, including early adoption of technologies such as ASP.NET MVC, jQuery, F#, AWS, and distributed source control.
- Developed solutions using Service Oriented Architecture (SOA), WCF, and Windows services. Integrated software with various cloud data and web service providers, including AWS, Authorize.NET, and Zencoder.
- Studied and implemented e-commerce best practices, web security standards, and PCI compliance.
---
## Mercury ML - Founder
> 2002 - 2004 (Lapeer, MI)
I got my start right out of high school helping to scale two companies out of working with unwieldy spreadsheets.
- Released two major versions of a VB6 application to catalog marketing affiliates, track orders, and calculate payouts.
- Wrote and administered affiliate websites in Classic ASP and VBScript.

View file

@ -0,0 +1 @@
export { default } from "./Experience"

30
src/ui/Home/Home.ts Normal file
View file

@ -0,0 +1,30 @@
import Layout from "ui/Layout"
import Markdown from "ui/shared/Markdown"
import intro from "./intro.md?raw"
import details from "./details.md?raw"
import skills from "./skills.md?raw"
import interests from "./interests.md?raw"
import Window from "ui/Layout/Window"
const title = "Dan Finch - About Me"
export default function Home() {
return Layout(
{ title, url: "/" },
Window(
{ title },
div.content(padding("8px"), [
h1("About Me"),
Markdown(intro),
aside.fieldBorderDisabled(
img({ src: "/pfp.jpg", width: "200" }),
),
Markdown(details),
h2("Skills"),
Markdown(skills),
h2("Interests"),
Markdown(interests),
]),
),
)
}

5
src/ui/Home/details.md Normal file
View file

@ -0,0 +1,5 @@
My first principle is **user experience**. Software is interesting because of what it can do for people, and we all put up with mediocre software every day. This dissatisfaction drives me to set a high standard for our work.
As a programmer, I've learned to make code **small, simple, and readable**. My aim is prolific and expeditious code output while maintaining quality. I make loosely-coupled components, understandable abstractions, and low-ceremony frameworks.
As a teammate and leader, I am devoted to **friendliness** and **respect**. I am a good communicator, love sharing my knowledge, and learning from others. I've given talks at local user groups and mentored at work and in my free time.

1
src/ui/Home/index.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "./Home"

4
src/ui/Home/interests.md Normal file
View file

@ -0,0 +1,4 @@
- Compilers, interpreters & parsers.
- Metaprogramming, language-oriented programming.
- Developer tools - hacking on build tools and bundlers.
- Automating self-hosted cloud tech.

1
src/ui/Home/intro.md Normal file
View file

@ -0,0 +1 @@
I own software end-to-end: building or renewing stacks and evaluating the tools and techniques that will serve the product well. I assist my teams in creating frameworks, automation, standards, and guidance that clear the path to delivering good software and user experiences. I am comfortable working hands-on across code, architecture, UX, and product.

8
src/ui/Home/skills.md Normal file
View file

@ -0,0 +1,8 @@
- Consciousness of mainstream & emerging technology and trends.
- Quickly vetting and adapting to new technologies - building and improving stacks.
- High standards for great user and developer experiences.
- Interacting directly and empathetically with stakeholders and end users.
- Managing work through all phases of a development cycle.
- Eliminating friction in processes through automation.
- Understanding all the areas of a codebase and recognizing where waste can be reduced.
- Coordinating large, complex upgrades and refactors.

18
src/ui/Layout/Head.ts Normal file
View file

@ -0,0 +1,18 @@
const DEFAULT_TITLE = "Dan Finch"
export default function Head(props: { title: string }, ...contents: Content[]) {
return head(
meta({ charset: "utf-8" }),
meta({
name: "viewport",
content: "width=device-width, initial-scale=1.0",
}),
link({
rel: "shortcut icon",
type: "image/png",
href: "/favicon.png",
}),
title(props.title || DEFAULT_TITLE),
contents,
)
}

50
src/ui/Layout/Layout.ts Normal file
View file

@ -0,0 +1,50 @@
import Head from "./Head"
import background from "assets/background.js?raw"
import css98 from "98se.css?url"
import Taskbar from "ui/Layout/Taskbar"
import Theme from "ui/Layout/Theme"
export default function Layout(
props: { title: string, url: string },
...contents: Content[]
) {
return [
doctype.html5,
html(
Head(props, [
link({ rel: "stylesheet", href: css98 }),
style(Theme()),
]),
body(
backgroundColor.black,
canvas(
{ id: "background" },
display.block,
position.fixed,
width("100%"),
height("100%"),
left(0),
top(0),
zIndex(-1),
),
div(
display.flex,
flexDirection.column,
height("100dvh"),
nav(
height.maxContent,
Taskbar({ url: props.url }),
),
main(
flex(1),
overflowY.hidden,
display.flex,
justifyContent.center,
contents,
)
)
),
script(raw(background)),
),
]
}

14
src/ui/Layout/Reset.ts Normal file
View file

@ -0,0 +1,14 @@
export default function Reset() {
return [
rule("*, *::before, *::after", [boxSizing.borderBox]),
rule("*:not(dialog)", [margin(0)]),
rule(
"input, button, textarea, select",
font("inherit"),
),
rule(
"p, h1, h2, h3, h4, h5, h6",
overflowWrap.breakWord,
),
]
}

101
src/ui/Layout/Taskbar.ts Normal file
View file

@ -0,0 +1,101 @@
import favicon from "public/favicon.png?url"
import linkedin from "public/icons/linkedin.svg?raw"
import github from "public/icons/github.svg?raw"
import aboutIcon from "public/icons/about.png?url"
import experienceIcon from "public/icons/experience.png?url"
import projectsIcon from "public/icons/projects.png?url"
import techIcon from "public/icons/tech.png?url"
const vr = hr(height("20px"), margin("0px 2px"))
export default function Taskbar({ url }: { url: string }) {
return div.taskbar.window(
textWrapMode.nowrap,
overflowX.hidden,
margin("10px"),
div(
display.flex,
alignItems.center,
gap("2px"),
div(
display.flex,
paddingLeft("4px"),
img(
{ src: favicon },
width("12px"),
height("12px"),
marginRight("4px"),
),
div(fontWeight.bold, "Dan Finch"),
),
vr,
a(
{ title: "LinkedIn" },
{ href: "https://www.linkedin.com/in/danfinch/" },
raw(linkedin),
),
a(
{ title: "GitHub" },
{ href: "https://github.com/errilaz" },
raw(github),
),
vr,
Task({
icon: aboutIcon,
title: "About",
href: "/",
active: url === "/",
}),
Task({
icon: projectsIcon,
title: "Projects",
href: "/projects",
active: url === "/projects",
}),
Task({
icon: experienceIcon,
title: "Experience",
href: "/experience",
active: url === "/experience",
}),
Task({
icon: techIcon,
title: "Tech",
href: "/tech",
active: url === "/tech",
}),
),
)
}
function Task({
icon,
title,
href,
active,
}: {
icon: string
title: Content
href?: string
active?: boolean
}) {
return a.button(
{ href },
active && className("active"),
padding("0 4px"),
textAlign.left,
div(
width("100%"),
gap("4px"),
display.flex,
justifyContent.flexStart,
img(
{ src: icon },
width("12px"),
height("12px"),
marginRight("4px"),
),
span(title),
),
)
}

39
src/ui/Layout/Theme.ts Normal file
View file

@ -0,0 +1,39 @@
import Reset from "./Reset"
export default function Theme() {
return [
Reset(),
$media(
"(max-width: 468px)",
rule(
".taskbar .button",
minWidth("unset"),
rule("img", display.none),
),
),
rule("a.button", [
textDecoration.none,
cursor._default,
display.flex,
alignItems.center,
]),
rule(h1, fontSize("20px")),
rule(h2, fontSize("16px")),
rule(ul, paddingInlineStart("30px")),
rule.content(
textAlign.justify,
fontFamily("Arial"),
fontSize("14px"),
rule(h1, fontSize("28px")),
rule(h2, fontSize("20px")),
rule(h3, fontSize("14px"),paddingLeft("30px")),
rule(aside,
float.right,
marginLeft("20px"),
paddingBottom(0),
),
rule("p, h1, h2, h3, ul", margin("12px 0")),
),
rule(iframe, border(0)),
]
}

32
src/ui/Layout/Window.ts Normal file
View file

@ -0,0 +1,32 @@
const DEFAULT_WIDTH = "770px"
export default function Window(
props: { title: Content; width?: Content; flat?: boolean },
...contents: Content[]
) {
return article.window(
opacity(0.9),
marginTop("16px"),
marginBottom("24px"),
marginLeft("10px"),
marginRight("10px"),
width(props.width ?? DEFAULT_WIDTH),
maxWidth(props.width ?? DEFAULT_WIDTH),
display.flex,
flexDirection.column,
div.titleBar(
div.titleBarText(props.title),
div.titleBarControls(
button({ "aria-label": "Minimize" }),
button({ "aria-label": "Maximize" }),
button({ "aria-label": "Close" }),
),
),
div.windowBody(
props.flat ? null : className("field-border"),
flex(1),
overflowY.auto,
contents,
),
)
}

1
src/ui/Layout/index.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "./Layout"

View file

@ -0,0 +1,21 @@
import Layout from "ui/Layout"
import Window from "ui/Layout/Window"
const title = "Dan Finch - Projects"
const sigitex = "https://sigitex.com"
export default function Projects() {
return Layout(
{ title, url: "/projects" },
Window(
{
title: span(title, " (", a({ href: sigitex }, color.yellow, "SIGITEX.COM"), ")")
},
backgroundColor.black,
display.flex,
iframe({ src: "https://sigitex.com" },
flex(1)
),
),
)
}

1
src/ui/Projects/index.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "./Projects"

118
src/ui/Tech/Sections.ts Normal file
View file

@ -0,0 +1,118 @@
export const languages = `
- TypeScript
- JavaScript
- C#
- F#
- SQL
- HTML
- CSS
`
export const platforms = `
- Node.js
- Deno
- Bun
- Linux
- Electron
- Browser Extensions
- React Native
- .NET
`
export const frontend = `
- React
- Vite
- Zustand
- Immer
- Webpack
- Parcel
- Emotion
- Styled Components
- Mithril
- Redux
- React Router
- React Query
- Vanilla Extract
- CSS Modules
- SCSS, Stylus, Less
`
export const backend = `
- Express
- Koa
- ASP.NET
- Fastify
- Hapi
- NestJS
- Cloudflare
- Firebase
- Supabase
- AWS
`
export const data = `
- Postgres
- Redis
- Sqlite
- MongoDB
- MySQL
- SQL Server
`
export const principles = `
- DDD
- FP
- Explicit data flow
- Modular design
- Testable boundaries
- Composability
- Separation of concerns
- SOLID
- DRY
- KISS
- YAGNI
`
export const architecture = `
- Domain modeling
- API design
- REST
- Event-driven systems
- Service boundaries
- State management
- Caching
- Data modeling
- Progressive enhancement
`
export const delivery = `
- CI/CD
- Build and release automation
- Docker
- GitHub Actions
- IaC
- Feature flags
- Observability
`
export const quality = `
- TDD
- Unit testing
- Integration testing
- E2E testing
- Code review
- Refactoring
- Accessibility
- Responsive design
`
export const collaboration = `
- Async-first
- Agile
- Pairing
- Mentoring
- Technical leadership
- Product collaboration
- Design collaboration
- Documentation
`

48
src/ui/Tech/Tech.ts Normal file
View file

@ -0,0 +1,48 @@
import Layout from "ui/Layout"
import Window from "ui/Layout/Window"
import Markdown from "ui/shared/Markdown"
import * as Sections from "./Sections"
const title = "Dan Finch - Expertise"
export default function Expertise() {
return Layout(
{ title, url: "/tech" },
Window(
{ title, flat: true },
div(
[
display.flex,
flexWrap.wrap,
marginRight("8px"),
paddingBottom("16px"),
gap("8px"),
],
Skillset("Core Languages", Sections.languages),
Skillset("Platforms", Sections.platforms),
Skillset("Frontend", Sections.frontend),
Skillset("Backend", Sections.backend),
Skillset("Data", Sections.data),
Skillset("Principles", Sections.principles),
Skillset("Architecture", Sections.architecture),
Skillset("Delivery", Sections.delivery),
Skillset("Quality", Sections.quality),
Skillset("Collaboration", Sections.collaboration),
),
),
)
}
function Skillset(title: string, markdown: Content) {
return div(
[
flex(1),
minWidth("250px"),
display.flex,
flexDirection.column,
gap("4px"),
],
h2(title),
div.fieldBorder(flex(1), Markdown(markdown)),
)
}

1
src/ui/Tech/index.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "./Tech"

View file

@ -0,0 +1,9 @@
import { Marked } from "marked"
import { gfmHeadingId } from "marked-gfm-heading-id"
export default function Markdown(md: string) {
return div.content(raw(marked.parse(md)))
}
const marked = new Marked({ gfm: true })
marked.use(gfmHeadingId())

16
tsconfig.base.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"lib": [
"esnext"
],
"types": [],
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"strict": true,
"outDir": "dist"
}
}

15
tsconfig.config.json Normal file
View file

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true,
"types": [
"bun"
]
},
"include": [
"*.config.ts",
"*.config.cjs",
"scripts/*.ts"
],
"files": []
}

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.src.json"
},
{
"path": "./tsconfig.test.json"
},
{
"path": "./tsconfig.config.json"
}
]
}

23
tsconfig.src.json Normal file
View file

@ -0,0 +1,23 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"composite": true,
"paths": {
"ui/*": [
"./src/ui/*"
],
"shared/*": [
"./src/shared/*"
],
"assets/*": [
"./assets/*"
],
"public/*": [
"./public/*"
]
}
},
"include": [
"src"
]
}

18
tsconfig.test.json Normal file
View file

@ -0,0 +1,18 @@
{
"extends": "./tsconfig.base.json",
"references": [
{
"path": "./tsconfig.src.json"
}
],
"compilerOptions": {
"composite": true,
"types": [
"bun"
]
},
"include": [
"tests"
],
"files": []
}