Initial commit: CV website with ElysiaJS, SQLite, and TailwindCSS

This commit is contained in:
Tuan-Dat Tran
2025-11-21 20:07:05 +01:00
parent 378487efc8
commit 88aeaa9002
14 changed files with 759 additions and 109 deletions

5
.gitignore vendored
View File

@@ -40,3 +40,8 @@ yarn-error.log*
**/*.log
package-lock.json
**/*.bun
# Database
cv.sqlite
cv.sqlite-shm
cv.sqlite-wal

136
bun.lock Normal file
View File

@@ -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=="],
}
}

View File

@@ -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"

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,41 @@
import * as elements from "typed-html";
export const BaseHtml = ({ children }: elements.Children) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CV Website</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {},
},
}
// Dark mode toggle script
const storedTheme = localStorage.getItem('theme');
if (storedTheme === 'dark' || (!storedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
function toggleTheme() {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
}
</script>
</head>
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white transition-colors duration-300">
${children}
</body>
</html>
`;

38
src/components/Layout.tsx Normal file
View File

@@ -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 (
<div class="container mx-auto p-4 max-w-4xl">
<header class="flex justify-between items-center py-4 mb-8">
<div class="text-xl font-bold">
<a href={`/${lang}`} class="hover:text-blue-500 dark:hover:text-blue-400">
My CV
</a>
</div>
<nav class="flex space-x-4 items-center">
<a href="/en" class={lang === "en" ? "font-bold text-blue-600 dark:text-blue-300" : "text-gray-600 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400"}>EN</a>
<a href="/de" class={lang === "de" ? "font-bold text-blue-600 dark:text-blue-300" : "text-gray-600 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400"}>DE</a>
<button onclick="toggleTheme()" class="p-2 rounded-md bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white transition-colors duration-200">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
{/* Sun icon */}
<path class="block dark:hidden" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 00-.707-.293h-.001c-.264 0-.524.103-.717.293l-.707.707a1 1 0 101.414 1.414l.707-.707c.2-.193.293-.453.293-.717v-.001zm-10.607.707l-.707-.707a1 1 0 00-1.414 1.414l.707.707c.193.2.453.293.717.293h.001a1 1 0 00.707-1.414zm-.707 4.95a1 1 0 000 1.414l.707.707a1 1 0 101.414-1.414l-.707-.707a1 1 0 00-1.414 0zM10 15a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm6.929-7.778l.707-.707a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414zM3.071 5.222l.707-.707a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414z" />
{/* Moon icon */}
<path class="hidden dark:block" d="M17.293 13.293A8 8 0 016.707 6.707a8.001 8.001 0 1010.586 6.586z" />
</svg>
</button>
</nav>
</header>
<main>
{children}
</main>
<footer class="text-center py-8 mt-12 text-gray-600 dark:text-gray-400 border-t border-gray-200 dark:border-gray-700">
<p>&copy; {new Date().getFullYear()} John Doe. All rights reserved.</p>
<p>Built with <a href="https://elysiajs.com/" target="_blank" class="text-blue-500 hover:underline">ElysiaJS</a> & <a href="https://tailwindcss.com/" target="_blank" class="text-blue-500 hover:underline">TailwindCSS</a>.</p>
</footer>
</div>
);
};

122
src/components/Sections.tsx Normal file
View File

@@ -0,0 +1,122 @@
import * as elements from "typed-html";
export const HeroSection = ({ profile }: any) => (
<section id="hero" class="text-center py-20 animate-fade-in relative overflow-hidden">
{/* Decorative background element */}
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-blue-100/50 dark:bg-blue-900/20 blur-[100px] rounded-full -z-10"></div>
<div class="relative inline-block mb-6 group">
<img
src={profile.avatar_url}
alt={profile.name}
class="w-40 h-40 rounded-full object-cover border-4 border-white dark:border-gray-800 shadow-xl transition-transform duration-500 group-hover:scale-105"
/>
<div class="absolute inset-0 rounded-full ring-2 ring-blue-500/20 dark:ring-blue-400/20 group-hover:ring-4 transition-all duration-500"></div>
</div>
<h1 class="text-6xl font-black mb-3 text-gray-900 dark:text-white tracking-tight">
{profile.name}
</h1>
<h2 class="text-2xl font-medium text-blue-600 dark:text-blue-400 mb-2">
{profile.job_title}
</h2>
{profile.location && (
<p class="text-sm text-gray-500 dark:text-gray-400 flex justify-center items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
{profile.location}
</p>
)}
<div class="mt-8 flex justify-center gap-4">
{profile.github_url && <a href={profile.github_url} target="_blank" class="p-2 text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white transition-colors"><svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg></a>}
{profile.email && <a href={`mailto:${profile.email}`} class="p-2 text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white transition-colors"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg></a>}
</div>
</section>
);
export const AboutSection = ({ summary }: { summary: string }) => (
<section id="about" class="py-12 max-w-2xl mx-auto">
<div class="bg-white dark:bg-gray-800/50 backdrop-blur-sm rounded-2xl p-8 shadow-sm border border-gray-100 dark:border-gray-700/50">
<h3 class="text-sm uppercase tracking-widest text-gray-500 dark:text-gray-400 font-bold mb-4">About</h3>
<p class="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
{summary}
</p>
</div>
</section>
);
export const ExperienceSection = ({ experience }: any) => (
<section id="experience" class="py-16">
<h2 class="text-3xl font-bold mb-12 text-gray-900 dark:text-white flex items-center gap-3">
<span class="w-8 h-1 bg-blue-500 rounded-full"></span>
Experience
</h2>
<div class="relative border-l-2 border-gray-200 dark:border-gray-700 ml-3 pl-8 space-y-12">
{experience.map((exp: any) => (
<div class="relative group">
{/* Hover highlight on line - optional subtle effect */}
<div class="absolute -left-[33px] top-2 w-3 h-3 bg-gray-200 dark:bg-gray-700 rounded-full border-2 border-white dark:border-gray-900 group-hover:bg-blue-500 group-hover:scale-125 transition-all duration-300"></div>
<div class="grid md:grid-cols-[1fr_3fr] gap-4">
<div class="text-sm text-gray-500 dark:text-gray-400 pt-1 font-mono">
{exp.start_date} {exp.end_date || "Present"}
</div>
<div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{exp.company_name}
</h3>
<div class="text-blue-600 dark:text-blue-400 font-medium mb-2">{exp.role}</div>
{exp.description && (
<p class="text-gray-600 dark:text-gray-300 leading-relaxed text-base">
{exp.description}
</p>
)}
</div>
</div>
</div>
))}
</div>
</section>
);
export const EducationSection = ({ education }: any) => (
<section id="education" class="py-12">
<h2 class="text-3xl font-bold mb-12 text-gray-900 dark:text-white flex items-center gap-3">
<span class="w-8 h-1 bg-purple-500 rounded-full"></span>
Education
</h2>
<div class="grid gap-6">
{education.map((edu: any) => (
<div class="bg-gray-50 dark:bg-gray-800/50 p-6 rounded-xl border border-gray-100 dark:border-gray-700 hover:border-purple-500/30 transition-colors">
<div class="flex flex-col md:flex-row md:justify-between md:items-baseline mb-2">
<h3 class="text-lg font-bold text-gray-900 dark:text-white">{edu.institution}</h3>
<span class="text-sm text-gray-500 font-mono">{edu.start_date} {edu.end_date || "Present"}</span>
</div>
<div class="text-purple-600 dark:text-purple-400 font-medium">{edu.degree}</div>
{edu.description && <p class="mt-2 text-gray-600 dark:text-gray-400 text-sm">{edu.description}</p>}
</div>
))}
</div>
</section>
);
export const SkillsSection = ({ skills }: any) => (
<section id="skills" class="py-16">
<h2 class="text-3xl font-bold mb-10 text-gray-900 dark:text-white flex items-center gap-3">
<span class="w-8 h-1 bg-green-500 rounded-full"></span>
Skills
</h2>
{/* 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 */}
<div class="flex flex-wrap gap-3">
{skills.map((skill: any) => (
<div class="px-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:shadow-md hover:-translate-y-0.5 transition-all duration-200 cursor-default">
<span class="text-xs text-gray-400 block mb-1 uppercase tracking-wider">{skill.category_display || skill.category}</span>
{skill.name}
</div>
))}
</div>
</section>
);

118
src/db/queries.ts Normal file
View File

@@ -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),
};
}

121
src/db/schema.ts Normal file
View File

@@ -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 };

86
src/db/seed.ts Normal file
View File

@@ -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();
}

View File

@@ -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}`
);

51
src/index.tsx Normal file
View File

@@ -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(
<BaseHtml>
<Layout lang={lang}>
<HeroSection profile={data.profile} />
{/* Separate About Section using the summary */}
{data.profile.summary && <AboutSection summary={data.profile.summary} />}
<div class="grid gap-12">
<ExperienceSection experience={data.experience} />
{/* Only render Education section if data exists */}
{data.education && data.education.length > 0 && (
<EducationSection education={data.education} />
)}
<SkillsSection skills={data.skills} />
</div>
</Layout>
</BaseHtml>
);
})
.listen(3000);
console.log(
`Elysia is running at http://${app.server?.hostname}:${app.server?.port}`
);

11
tailwind.config.js Normal file
View File

@@ -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: [],
}

View File

@@ -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 '<reference>'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
}
}