diff --git a/.gitignore b/.gitignore index 87e5610..f733819 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,9 @@ yarn-error.log* **/*.tgz **/*.log package-lock.json -**/*.bun \ No newline at end of file +**/*.bun + +# Database +cv.sqlite +cv.sqlite-shm +cv.sqlite-wal diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..07492f2 --- /dev/null +++ b/bun.lock @@ -0,0 +1,136 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "dependencies": { + "@elysiajs/html": "^1.4.0", + "@kitajs/ts-html-plugin": "^4.1.3", + "autoprefixer": "^10.4.22", + "elysia": "^1.4.16", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", + "typed-html": "^3.0.1", + }, + "devDependencies": { + "bun-types": "latest", + }, + }, + }, + "packages": { + "@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="], + + "@elysiajs/html": ["@elysiajs/html@1.4.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-j4jFqGEkIC8Rg2XiTOujb9s0WLnz1dnY/4uqczyCdOVruDeJtGP+6+GvF0A76SxEvltn8UR1yCUnRdLqRi3vuw=="], + + "@kitajs/html": ["@kitajs/html@4.2.11", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-gOe+zzCZKN2fPT1FUK32mHsr21ILcAOUUux/yDqQthInW8egN8RuxVp+zP3KhwWETVACkurBiKV9RWuNw+ceiw=="], + + "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.3", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-NlYrID5yMxfRKiO1eiiSC4MWveKe0ffoCJOZm4idNOqwimmLXr0g1NmvCcquOU2XLRrgzynxZqw6rhwR5CY5Nw=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + + "@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "autoprefixer": ["autoprefixer@10.4.22", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-lite": "^1.0.30001754", "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA=="], + + "browserslist": ["browserslist@4.28.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", "electron-to-chromium": "^1.5.249", "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ=="], + + "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001756", "", {}, "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.259", "", {}, "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ=="], + + "elysia": ["elysia@1.4.16", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.3", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-KZtKN160/bdWVKg2hEgyoNXY8jRRquc+m6PboyisaLZL891I+Ufb7Ja6lDAD7vMQur8sLEWIcidZOzj5lWw9UA=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "exact-mirror": ["exact-mirror@0.2.3", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-aLdARfO0W0ntufjDyytUJQMbNXoB9g+BbA8KcgIq4XOOTYRw48yUGON/Pr64iDrYNZKcKvKbqE0MPW56FF2BXA=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "file-type": ["file-type@21.1.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + + "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], + + "token-types": ["token-types@6.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typed-html": ["typed-html@3.0.1", "", {}, "sha512-JKCM9zTfPDuPqQqdGZBWSEiItShliKkBFg5c6yOR8zth43v763XkAzTWaOlVqc0Y6p9ee8AaAbipGfUnCsYZUA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + + "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + } +} diff --git a/package.json b/package.json index 9bdc33a..6fcad5a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,13 @@ "dev": "bun run --watch src/index.ts" }, "dependencies": { - "elysia": "latest" + "@elysiajs/html": "^1.4.0", + "@kitajs/ts-html-plugin": "^4.1.3", + "autoprefixer": "^10.4.22", + "elysia": "^1.4.16", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", + "typed-html": "^3.0.1" }, "devDependencies": { "bun-types": "latest" diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..fbe14a4 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/src/components/BaseHtml.tsx b/src/components/BaseHtml.tsx new file mode 100644 index 0000000..ab83c7c --- /dev/null +++ b/src/components/BaseHtml.tsx @@ -0,0 +1,41 @@ +import * as elements from "typed-html"; + +export const BaseHtml = ({ children }: elements.Children) => ` + + + + + + CV Website + + + + + ${children} + + +`; diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..8570a9a --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,38 @@ +import * as elements from "typed-html"; + +interface LayoutProps extends elements.Children { + lang: "en" | "de"; +} + +export const Layout = ({ children, lang }: LayoutProps) => { + return ( +
+
+ + +
+
+ {children} +
+ +
+ ); +}; diff --git a/src/components/Sections.tsx b/src/components/Sections.tsx new file mode 100644 index 0000000..af25a05 --- /dev/null +++ b/src/components/Sections.tsx @@ -0,0 +1,122 @@ +import * as elements from "typed-html"; + +export const HeroSection = ({ profile }: any) => ( +
+ {/* Decorative background element */} +
+ +
+ {profile.name} +
+
+ +

+ {profile.name} +

+

+ {profile.job_title} +

+ {profile.location && ( +

+ + {profile.location} +

+ )} + +
+ {profile.github_url && } + {profile.email && } +
+
+); + +export const AboutSection = ({ summary }: { summary: string }) => ( +
+
+

About

+

+ {summary} +

+
+
+); + +export const ExperienceSection = ({ experience }: any) => ( +
+

+ + Experience +

+
+ {experience.map((exp: any) => ( +
+ {/* Hover highlight on line - optional subtle effect */} +
+ +
+
+ {exp.start_date} — {exp.end_date || "Present"} +
+
+

+ {exp.company_name} +

+
{exp.role}
+ {exp.description && ( +

+ {exp.description} +

+ )} +
+
+
+ ))} +
+
+); + +export const EducationSection = ({ education }: any) => ( +
+

+ + Education +

+
+ {education.map((edu: any) => ( +
+
+

{edu.institution}

+ {edu.start_date} — {edu.end_date || "Present"} +
+
{edu.degree}
+ {edu.description &&

{edu.description}

} +
+ ))} +
+
+); + +export const SkillsSection = ({ skills }: any) => ( +
+

+ + Skills +

+ + {/* Grouping by category could be done here if the data supports it nicely, + but for now we'll do a clean tag cloud with categories visually distinct if needed */} + +
+ {skills.map((skill: any) => ( +
+ {skill.category_display || skill.category} + {skill.name} +
+ ))} +
+
+); diff --git a/src/db/queries.ts b/src/db/queries.ts new file mode 100644 index 0000000..7544a56 --- /dev/null +++ b/src/db/queries.ts @@ -0,0 +1,118 @@ +import { db } from "./schema"; + +interface ProfileTranslation { + name: string; + job_title: string; + summary: string | null; + location: string | null; +} + +interface Profile extends ProfileTranslation { + email: string; + phone: string | null; + website: string | null; + github_url: string | null; + linkedin_url: string | null; + avatar_url: string | null; +} + +interface ExperienceTranslation { + company_name: string; + role: string; + description: string | null; + location: string | null; +} + +interface Experience extends ExperienceTranslation { + start_date: string; + end_date: string | null; + company_url: string | null; +} + +interface SkillTranslation { + name: string; + category_display: string | null; +} + +interface Skill extends SkillTranslation { + category: string; + icon: string | null; + display_order: number; +} + +export function getProfile(lang: string): Profile | null { + const profile = db.query(` + SELECT p.email, p.phone, p.website, p.github_url, p.linkedin_url, p.avatar_url, + pt.name, pt.job_title, pt.summary, pt.location + FROM profile p + JOIN profile_translations pt ON p.id = pt.profile_id + WHERE pt.language_code = $lang + `).get({ $lang: lang }) as Profile | null; + return profile; +} + +export function getExperience(lang: string): Experience[] { + const experience = db.query(` + SELECT e.start_date, e.end_date, e.company_url, e.display_order, + et.company_name, et.role, et.description, et.location + FROM experience e + JOIN experience_translations et ON e.id = et.experience_id + WHERE et.language_code = $lang + ORDER BY e.display_order ASC, e.start_date DESC + `).all({ $lang: lang }) as Experience[]; + return experience; +} + +export function getSkills(lang: string): Skill[] { + const skills = db.query(` + SELECT s.category, s.icon, s.display_order, + st.name, st.category_display + FROM skills s + JOIN skill_translations st ON s.id = st.skill_id + WHERE st.language_code = $lang + ORDER BY s.display_order ASC, s.category ASC, st.name ASC + `).all({ $lang: lang }) as Skill[]; + return skills; +} + +export function getEducation(lang: string): Education[] { + + const education = db.query(` + + SELECT e.start_date, e.end_date, e.institution_url, e.display_order, + + et.institution, et.degree, et.description + + FROM education e + + JOIN education_translations et ON e.id = et.education_id + + WHERE et.language_code = $lang + + ORDER BY e.display_order ASC, e.start_date DESC + + `).all({ $lang: lang }) as Education[]; + + return education; + +} + + + +export function getAllData(lang: string) { + + return { + + profile: getProfile(lang), + + experience: getExperience(lang), + + education: getEducation(lang), + + skills: getSkills(lang), + + }; + +} + + diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..17ab315 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,121 @@ +import { Database } from "bun:sqlite"; + +const db = new Database("cv.sqlite", { create: true }); + +// Enable foreign keys +db.run("PRAGMA foreign_keys = ON;"); + +export function initDB() { + // 1. Languages Table (to ensure referential integrity) + db.run(` + CREATE TABLE IF NOT EXISTS languages ( + code TEXT PRIMARY KEY + ); + `); + + // 2. Profile (Singleton - structural info) + db.run(` + CREATE TABLE IF NOT EXISTS profile ( + id INTEGER PRIMARY KEY CHECK (id = 1), -- Ensure only one profile exists + email TEXT NOT NULL, + phone TEXT, + website TEXT, + github_url TEXT, + linkedin_url TEXT, + avatar_url TEXT + ); + `); + + // 3. Profile Translations + db.run(` + CREATE TABLE IF NOT EXISTS profile_translations ( + profile_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + name TEXT NOT NULL, + job_title TEXT NOT NULL, + summary TEXT, + location TEXT, + PRIMARY KEY (profile_id, language_code), + FOREIGN KEY (profile_id) REFERENCES profile(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); + + // 4. Experience (Structural) + db.run(` + CREATE TABLE IF NOT EXISTS experience ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + start_date TEXT NOT NULL, -- ISO8601 YYYY-MM + end_date TEXT, -- NULL = Current + company_url TEXT, + display_order INTEGER DEFAULT 0 + ); + `); + + // 5. Experience Translations + db.run(` + CREATE TABLE IF NOT EXISTS experience_translations ( + experience_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + company_name TEXT NOT NULL, + role TEXT NOT NULL, + description TEXT, -- Supports Markdown/HTML + location TEXT, + PRIMARY KEY (experience_id, language_code), + FOREIGN KEY (experience_id) REFERENCES experience(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); + + // 6. Education (Structural) + db.run(` + CREATE TABLE IF NOT EXISTS education ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + start_date TEXT NOT NULL, + end_date TEXT, + institution_url TEXT, + display_order INTEGER DEFAULT 0 + ); + `); + + // 7. Education Translations + db.run(` + CREATE TABLE IF NOT EXISTS education_translations ( + education_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + institution TEXT NOT NULL, + degree TEXT NOT NULL, + description TEXT, + PRIMARY KEY (education_id, language_code), + FOREIGN KEY (education_id) REFERENCES education(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); + + // 8. Skills (Categories & Items) + // Simply storing skills as items with a category. + db.run(` + CREATE TABLE IF NOT EXISTS skills ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category TEXT NOT NULL, -- e.g., "frontend", "backend", "tools" (Internal key) + icon TEXT, -- class name for an icon library + display_order INTEGER DEFAULT 0 + ); + `); + + db.run(` + CREATE TABLE IF NOT EXISTS skill_translations ( + skill_id INTEGER NOT NULL, + language_code TEXT NOT NULL, + name TEXT NOT NULL, -- The display name + category_display TEXT, -- "Frontend Development" vs "Frontend Entwicklung" + PRIMARY KEY (skill_id, language_code), + FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE, + FOREIGN KEY (language_code) REFERENCES languages(code) + ); + `); + + console.log("Database schema initialized."); +} + +export { db }; diff --git a/src/db/seed.ts b/src/db/seed.ts new file mode 100644 index 0000000..fa26e3a --- /dev/null +++ b/src/db/seed.ts @@ -0,0 +1,86 @@ +import { db, initDB } from "./schema"; + +export function seedDB() { + // Initialize tables first + initDB(); + + // Check if data exists to avoid duplicates + const check = db.query("SELECT count(*) as count FROM languages").get() as { count: number }; + if (check.count > 0) { + console.log("Database already seeded."); + return; + } + + console.log("Seeding database..."); + + // 1. Languages + const insertLang = db.prepare("INSERT INTO languages (code) VALUES ($code)"); + insertLang.run({ $code: "en" }); + insertLang.run({ $code: "de" }); + + // 2. Profile + db.run(` + INSERT INTO profile (id, email, phone, website, github_url, linkedin_url, avatar_url) + VALUES (1, 'contact@johndoe.dev', '+49 123 456789', 'https://johndoe.dev', 'https://github.com/johndoe', 'https://linkedin.com/in/johndoe', 'https://placehold.co/400') + `); + + const insertProfileTrans = db.prepare(` + INSERT INTO profile_translations (profile_id, language_code, name, job_title, summary, location) + VALUES ($pid, $code, $name, $title, $summary, $loc) + `); + + insertProfileTrans.run({ + $pid: 1, $code: "en", $name: "John Doe", $title: "Full Stack Developer", + $summary: "Passionate developer building minimalist and high-performance web applications.", + $loc: "Berlin, Germany" + }); + insertProfileTrans.run({ + $pid: 1, $code: "de", $name: "John Doe", $title: "Full Stack Entwickler", + $summary: "Leidenschaftlicher Entwickler für minimalistische und hochperformante Webanwendungen.", + $loc: "Berlin, Deutschland" + }); + + // 3. Experience + const insertExp = db.prepare(` + INSERT INTO experience (start_date, end_date, company_url, display_order) + VALUES ($start, $end, $url, $order) + RETURNING id + `); + + // Job 1 + const job1 = insertExp.get({ $start: "2023-01", $end: null, $url: "https://techcorp.com", $order: 1 }) as { id: number }; + + const insertExpTrans = db.prepare(` + INSERT INTO experience_translations (experience_id, language_code, company_name, role, description) + VALUES ($eid, $code, $comp, $role, $desc) + `); + + insertExpTrans.run({ + $eid: job1.id, $code: "en", $comp: "Tech Corp", $role: "Senior Engineer", + $desc: "Leading the frontend team and migrating legacy codebase to ElysiaJS." + }); + insertExpTrans.run({ + $eid: job1.id, $code: "de", $comp: "Tech Corp", $role: "Senior Entwickler", + $desc: "Leitung des Frontend-Teams und Migration der Legacy-Codebasis zu ElysiaJS." + }); + + // 4. Skills + const insertSkill = db.prepare(` + INSERT INTO skills (category, icon, display_order) VALUES ($cat, $icon, $order) RETURNING id + `); + const insertSkillTrans = db.prepare(` + INSERT INTO skill_translations (skill_id, language_code, name, category_display) + VALUES ($sid, $code, $name, $catDisplay) + `); + + const s1 = insertSkill.get({ $cat: "tech", $icon: "code", $order: 1 }) as { id: number }; + insertSkillTrans.run({ $sid: s1.id, $code: "en", $name: "TypeScript", $catDisplay: "Technologies" }); + insertSkillTrans.run({ $sid: s1.id, $code: "de", $name: "TypeScript", $catDisplay: "Technologien" }); + + console.log("Seeding complete."); +} + +// Allow running directly: bun src/db/seed.ts +if (import.meta.main) { + seedDB(); +} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 9c1f7a1..0000000 --- a/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Elysia } from "elysia"; - -const app = new Elysia().get("/", () => "Hello Elysia").listen(3000); - -console.log( - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` -); diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..c0d64fc --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,51 @@ +import { Elysia, NotFoundError } from "elysia"; +import { html } from "@elysiajs/html"; +import * as elements from "typed-html"; +import { BaseHtml } from "./components/BaseHtml"; +import { Layout } from "./components/Layout"; +import { HeroSection, AboutSection, ExperienceSection, EducationSection, SkillsSection } from "./components/Sections"; +import { getAllData } from "./db/queries"; + +const app = new Elysia() + .use(html()) + .get("/", () => { + return Response.redirect("/en"); // Default to English + }) + .get("/:lang", ({ params, html, set }) => { + const lang = params.lang as "en" | "de"; + if (!["en", "de"].includes(lang)) { + throw new NotFoundError(); + } + + const data = getAllData(lang); + if (!data.profile) { + throw new NotFoundError("Profile data not found for selected language."); + } + + return html( + + + + + {/* Separate About Section using the summary */} + {data.profile.summary && } + +
+ + + {/* Only render Education section if data exists */} + {data.education && data.education.length > 0 && ( + + )} + + +
+
+
+ ); + }) + .listen(3000); + +console.log( + `Elysia is running at http://${app.server?.hostname}:${app.server?.port}` +); diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..6a4a71f --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./src/**/*.{ts,tsx,html}", + ], + darkMode: 'class', // Enables dark mode based on 'dark' class in HTML + theme: { + extend: {}, + }, + plugins: [], +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 1ca2350..7542f04 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,103 +1,19 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - - /* Modules */ - "module": "ES2022", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react", + "jsxFactory": "elements.createElement", + "jsxFragmentFactory": "elements.Fragment", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true } -} +} \ No newline at end of file