feat: portfolio done + minimal working blog

Signed-off-by: Ameya Shenoy <shenoy.ameya@gmail.com>
This commit is contained in:
Ameya Shenoy 2025-06-23 09:22:26 +05:30
parent 360fb2d3d6
commit e74f75ae4b
9 changed files with 962 additions and 64 deletions

View file

@ -13,13 +13,17 @@
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dompurify": "^3.2.6",
"gsap": "^3.13.0",
"lucide-react": "^0.511.0",
"marked": "^15.0.12",
"marked-shiki": "^1.2.0",
"matter-js": "^0.20.0",
"next": "15.3.3",
"next-themes": "^0.4.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"shiki": "^3.7.0",
"tailwind-merge": "^3.3.0",
"tw-to-css": "^0.0.12"
},
@ -771,6 +775,17 @@
"node": ">= 8"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -1314,6 +1329,73 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@shikijs/core": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.7.0.tgz",
"integrity": "sha512-yilc0S9HvTPyahHpcum8eonYrQtmGTU0lbtwxhA6jHv4Bm1cAdlPFRCJX4AHebkCm75aKTjjRAW+DezqD1b/cg==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.7.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.7.0.tgz",
"integrity": "sha512-0t17s03Cbv+ZcUvv+y33GtX75WBLQELgNdVghnsdhTgU3hVcWcMsoP6Lb0nDTl95ZJfbP1mVMO0p3byVh3uuzA==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.7.0",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.3"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.7.0.tgz",
"integrity": "sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.7.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.7.0.tgz",
"integrity": "sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.7.0"
}
},
"node_modules/@shikijs/themes": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.7.0.tgz",
"integrity": "sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==",
"license": "MIT",
"dependencies": {
"@shikijs/types": "3.7.0"
}
},
"node_modules/@shikijs/types": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.7.0.tgz",
"integrity": "sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==",
"license": "MIT",
"dependencies": {
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/@shikijs/vscode-textmate": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT"
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@ -1605,6 +1687,24 @@
"tailwindcss": "4.1.8"
}
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/mdast": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/node": {
"version": "20.17.57",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
@ -1635,6 +1735,25 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
@ -1775,6 +1894,36 @@
],
"license": "CC-BY-4.0"
},
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-html4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -1891,6 +2040,16 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -1933,6 +2092,15 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -1949,6 +2117,19 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -1961,6 +2142,15 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -2156,6 +2346,52 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-to-html": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^3.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"stringify-entities": "^4.0.0",
"zwitch": "^2.0.4"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@ -2557,12 +2793,55 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/marked": {
"version": "15.0.12",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/marked-shiki": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/marked-shiki/-/marked-shiki-1.2.0.tgz",
"integrity": "sha512-N924hp8veE6Mc91g5/kCNVoTU7TkeJfB2G2XEWb+k1fVA0Bck2T0rVt93d39BlOYH6ohP4Q9BFlPk+UkblhXbg==",
"license": "MIT",
"peerDependencies": {
"marked": ">=7.0.0",
"shiki": ">=1.0.0"
}
},
"node_modules/matter-js": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.20.0.tgz",
"integrity": "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA==",
"license": "MIT"
},
"node_modules/mdast-util-to-hast": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"@ungap/structured-clone": "^1.0.0",
"devlop": "^1.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"trim-lines": "^3.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -2572,6 +2851,95 @@
"node": ">= 8"
}
},
"node_modules/micromark-util-character": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-util-encode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-sanitize-uri": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-encode": "^2.0.0",
"micromark-util-symbol": "^2.0.0"
}
},
"node_modules/micromark-util-symbol": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromark-util-types": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
"funding": [
{
"type": "GitHub Sponsors",
"url": "https://github.com/sponsors/unifiedjs"
},
{
"type": "OpenCollective",
"url": "https://opencollective.com/unified"
}
],
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@ -2786,6 +3154,23 @@
"node": ">= 6"
}
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz",
"integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==",
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.1",
"regex": "^6.0.1",
"regex-recursion": "^6.0.2"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@ -2981,6 +3366,16 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -3116,6 +3511,30 @@
"node": ">=8.10.0"
}
},
"node_modules/regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
"integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-recursion": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
"integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
"license": "MIT",
"dependencies": {
"regex-utilities": "^2.3.0"
}
},
"node_modules/regex-utilities": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -3254,6 +3673,22 @@
"node": ">=8"
}
},
"node_modules/shiki": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-3.7.0.tgz",
"integrity": "sha512-ZcI4UT9n6N2pDuM2n3Jbk0sR4Swzq43nLPgS/4h0E3B/NrFn2HKElrDtceSf8Zx/OWYOo7G1SAtBLypCp+YXqg==",
"license": "MIT",
"dependencies": {
"@shikijs/core": "3.7.0",
"@shikijs/engine-javascript": "3.7.0",
"@shikijs/engine-oniguruma": "3.7.0",
"@shikijs/langs": "3.7.0",
"@shikijs/themes": "3.7.0",
"@shikijs/types": "3.7.0",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@ -3285,6 +3720,16 @@
"node": ">=0.10.0"
}
},
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@ -3352,6 +3797,20 @@
"node": ">=8"
}
},
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"license": "MIT",
"dependencies": {
"character-entities-html4": "^2.0.0",
"character-entities-legacy": "^3.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
@ -3524,6 +3983,16 @@
"node": ">=8.0"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -3703,6 +4172,74 @@
"dev": true,
"license": "MIT"
},
"node_modules/unist-util-is": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
"integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
"integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
@ -3752,6 +4289,34 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
"integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-stringify-position": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -3879,6 +4444,16 @@
"engines": {
"node": ">= 14.6"
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
}
}
}

View file

@ -14,13 +14,17 @@
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dompurify": "^3.2.6",
"gsap": "^3.13.0",
"lucide-react": "^0.511.0",
"marked": "^15.0.12",
"marked-shiki": "^1.2.0",
"matter-js": "^0.20.0",
"next": "15.3.3",
"next-themes": "^0.4.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"shiki": "^3.7.0",
"tailwind-merge": "^3.3.0",
"tw-to-css": "^0.0.12"
},

View file

@ -0,0 +1,24 @@
import MarkdownRenderer from "@/components/MarkdownRenderer";
import { promises as fs } from "fs";
export default async function BlogPage() {
try {
// Read markdown file from project root
const filePath = "src/app/blog/docker_primer/content.md";
const markdownContent = await fs.readFile(filePath, "utf8");
return (
<main className="flex flex-1 flex-col justify-end items-center font-[family-name:var(--font-inter-sans)] pt-10 md:pt-20 pb-25 md:pb-0">
<div className="md:w-[786px] w-[95%]">
<div className="p-5 pt-10 rounded-lg markdown">
<MarkdownRenderer markdown={markdownContent} />
</div>
</div>
</main>
);
} catch (error) {
console.error("Error loading markdown:", error);
return <div>Failed to load content</div>;
}
}

View file

@ -138,6 +138,111 @@
color: white;
pointer-events: none;
}
pre.shiki {
position: relative;
z-index: 1;
padding: 0.5rem 0;
background: transparent;
border-radius: 0.5rem;
margin: 20px 0px;
}
pre.shiki code {
display: block;
padding: 2rem 1rem 0.5rem 1rem;
transition: color 0.5s;
overflow-x: auto; /* Enables horizontal scrolling */
white-space: pre; /* Prevents line wrapping */
max-width: 100%; /* Ensures it doesn't overflow its container */
}
pre.shiki .copy-code-btn {
position: absolute;
right: 8px;
top: 8px;
z-index: 10;
background: #333;
color: #fff;
border: none;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
}
pre.shiki .filetype-code-btn {
position: absolute;
left: 8px;
top: 8px;
z-index: 10;
background: rgba(255, 255, 255, 0.15);
color: #fff;
border-radius: 4px;
padding: 2px 8px;
font-size: 12px;
font-family: monospace;
text-transform: uppercase;
pointer-events: none;
user-select: none;
}
.markdown hr {
height: 4px;
background: var(--sidebar-accent);
border: none;
}
.markdown p {
margin: 1em 0;
}
.markdown ul,
.markdown ol {
margin: 1em 0 1em 2em;
padding: 0;
list-style-position: outside;
}
.markdown ul {
list-style-type: disc;
}
.markdown ol {
list-style-type: decimal;
}
.markdown li {
margin: 0.3em 0;
line-height: 1.6;
}
.markdown blockquote {
border-left: 4px solid #ccc;
margin: 1em 0;
padding: 0.5em 1em;
background: var(--sidebar-accent);
}
.markdown table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
}
.markdown th,
.markdown td {
border: 1px solid #ddd;
padding: 0.5em 1em;
text-align: left;
}
.markdown th {
background: var(--sidebar-accent);
font-weight: bold;
}
.markdown a {
text-decoration: underline;
}
}
@media (min-width: 768px) {

View file

@ -1,4 +1,4 @@
import { MapPin, Code, Briefcase, FileUser } from "lucide-react";
import { MapPin, Code, Briefcase, Download } from "lucide-react";
import { headers } from "next/headers";
import { Icon } from "@iconify/react";
import Link from "next/link";
@ -23,13 +23,6 @@ export default async function Portfolio() {
TypeScript: "vscode-icons:file-type-typescript-official",
Golang: "material-icon-theme:go",
},
"Web Frameworks": {
FastAPI: "devicon:fastapi",
Django: "material-icon-theme:django",
Flask: "simple-icons:flask",
ReactJS: "material-icon-theme:react",
"Next.js": "devicon:nextjs",
},
AI: {
LangChain: "simple-icons:langchain",
Livekit: null,
@ -47,8 +40,25 @@ export default async function Portfolio() {
qdrant: "logos:qdrant-icon",
MySQL: "logos:mysql-icon",
},
"Cloud Providers": {
AWS: "logos:aws",
"Microsoft Azure": "material-icon-theme:azure",
"Google Cloud Platform GCP": "material-icon-theme:gcp",
DigitalOcean: "devicon:digitalocean",
// Scaleway: "simple-icons:scaleway",
// Hetzner: "simple-icons:hetzner",
Cloudflare: "devicon:cloudflare",
},
};
const skillsRight = {
"Web Frameworks": {
FastAPI: "devicon:fastapi",
Django: "material-icon-theme:django",
"Next.js": "devicon:nextjs",
Flask: "simple-icons:flask",
ReactJS: "material-icon-theme:react",
},
DevOps: {
Kubernetes: "material-icon-theme:kubernetes",
Ansible: "devicon:ansible",
@ -70,15 +80,6 @@ export default async function Portfolio() {
Kafka: "devicon:apachekafka",
// Selenium: "devicon:selenium",
},
"Cloud Providers": {
AWS: "logos:aws",
"Microsoft Azure": "material-icon-theme:azure",
"Google Cloud Platform GCP": "material-icon-theme:gcp",
DigitalOcean: "devicon:digitalocean",
// Scaleway: "simple-icons:scaleway",
// Hetzner: "simple-icons:hetzner",
Cloudflare: "devicon:cloudflare",
},
Robotics: {
"Robot Operating System ROS": "devicon:ros",
},
@ -144,61 +145,85 @@ export default async function Portfolio() {
];
return (
<main className="flex items-top justify-center md:pt-20 md:pb-0 pb-20 font-[family-name:var(--font-inter-sans)]">
<main className="flex items-top justify-center font-[family-name:var(--font-inter-sans)] pt-10 md:pt-20 pb-25 md:pb-0">
<div className="">
<div className="p-5 uppercase tracking-[0.2em] text-gray-500 dark:text-gray-100 flex items-center justify-between">
<div className="uppercase tracking-[0.2em] text-gray-500 dark:text-gray-100 flex items-center justify-between pb-20">
<div>About Me</div>
<Link href="https://resume.codingcoffee.me">
<div className="flex border-gray-200 border-2 p-2 ">
Download
<FileUser />
<Link
href="https://resume.codingcoffee.me"
>
<div className="flex border-gray-200 border-2 p-2 pointer-cursor underline">
Resume
<Download />
</div>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 md:w-[40vw] w-[95vw] md:min-w-3xl md:gap-5 md:p-5">
<div className="grid grid-cols-1 md:grid-cols-2 md:w-[40vw] w-[95vw] md:min-w-3xl">
<div>
<div>
<div className="text-5xl md:text-6xl">Hi, I'm</div>
<div className="pb-10">
<div className="text-5xl md:text-6xl pb-5">Hi, I'm</div>
<div className="text-5xl md:text-6xl text-red-500">
Ameya Shenoy
</div>
</div>
<div className="pb-5 text-gray-500 dark:text-gray-100 text-lg">
<div className="flex items-center pt-5">
<div className="text-gray-500 dark:text-gray-100 text-lg pb-10">
<div className="flex items-center pb-2">
<MapPin className="stroke-1 text-red-500" />
<span className="pl-2">Based in India</span>
</div>
<div className="flex items-center pt-5">
<div className="flex items-center pb-2">
<Code className="stroke-1 text-red-500" />
<span className="pl-2">
Principal Engineer / Engineering Lead
</span>
</div>
<div className="flex items-center pt-5">
<div className="flex items-center pb-2">
<Briefcase className="stroke-1 text-red-500" />
<span className="pl-2">8+ years experience</span>
</div>
</div>
</div>
<div className="md:text-2xl/10 text-xl/8">
<div className="md:text-2xl/10 text-xl/8 pb-20">
Engineering lead with 8+ years of expertise in designing scalable
system architecture, leading high performing teams, and delivering
ai driven solutions while also maintaining the role of an individual
contributor.
</div>
</div>
<div className="md:w-[40vw] w-[95vw] md:min-w-3xl md:p-5">
<div className="text-3xl">Key Skills</div>
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-5">
<div className="md:w-[40vw] w-[95vw] md:min-w-3xl">
<div className="text-3xl pb-10">Work Experience</div>
{workExperience.map((job, index) => (
<div key={index} className="">
<div className="flex items-center justify-between pb-5">
<Link
href={job.link}
target="_blank"
rel="noopener noreferrer"
className="text-xl underline"
>
{job.company}
</Link>
<p className="text-xl">{job.tenure}</p>
</div>
<p className="text-2xl text-red-500 pb-5">{job.title}</p>
<p className="text-gray-500 dark:text-gray-100 text-justify text-xl/8 pb-8">
{job.summary}
</p>
</div>
))}
</div>
<div className="md:w-[40vw] w-[95vw] md:min-w-3xl">
<div className="text-3xl pb-10 gap-10">Key Skills</div>
<div className="grid grid-cols-1 md:grid-cols-2">
<div>
{Object.entries(skillsLeft).map(([topic, skillsMap]) => (
<div key={topic}>
<div className="text-xl pt-5 pb-3">{topic}</div>
<div className="flex items-center flex-wrap gap-1">
<div className="text-xl pb-5">{topic}</div>
<div className="flex items-center flex-wrap pb-5 gap-2">
{Object.entries(skillsMap).map(([language, icon]) => (
<span
key={language}
className="flex items-center gap-2 bg-red-50 dark:bg-red-900 px-3 py-1 rounded-full text-gray-700 dark:text-white"
className="flex items-center bg-red-50 dark:bg-red-900 px-3 py-1 rounded-full text-gray-700 dark:text-white gap-2"
>
<Icon icon={icon} className="inline-block" /> {language}
</span>
@ -210,12 +235,12 @@ export default async function Portfolio() {
<div>
{Object.entries(skillsRight).map(([topic, skillsMap]) => (
<div key={topic}>
<div className="text-xl pt-5 pb-3">{topic}</div>
<div className="flex items-center flex-wrap gap-1">
<div className="text-xl pb-5">{topic}</div>
<div className="flex items-center flex-wrap pb-5 gap-2">
{Object.entries(skillsMap).map(([language, icon]) => (
<span
key={language}
className="flex items-center gap-2 bg-red-50 dark:bg-red-900 px-3 py-1 rounded-full text-gray-700 dark:text-white"
className="flex items-center bg-red-50 dark:bg-red-900 px-3 py-1 rounded-full text-gray-700 dark:text-white gap-2"
>
<Icon icon={icon} className="inline-block" /> {language}
</span>
@ -226,28 +251,6 @@ export default async function Portfolio() {
</div>
</div>
</div>
<div className="md:w-[40vw] w-[95vw] md:min-w-3xl md:gap-5 md:p-5">
<div className="text-3xl">Work Experience</div>
{workExperience.map((job, index) => (
<div key={index} className="p-3">
<div className="flex items-center justify-between">
<Link
href={job.link}
target="_blank"
rel="noopener noreferrer"
className="text-xl hover:underline"
>
{job.company}
</Link>
<p className="text-xl">{job.tenure}</p>
</div>
<p className="text-2xl pt-2 text-red-500">{job.title}</p>
<p className="pt-2 text-gray-500 dark:text-gray-100 text-justify">
{job.summary}
</p>
</div>
))}
</div>
</div>
</main>
);

View file

@ -5,7 +5,8 @@ import gsap from "gsap";
const GSAPCursor: FC = () => {
const cursorRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isHoveringLink, setIsHoveringLink] = useState(false);
// For touch devices, return null
if (typeof window !== "undefined" && "ontouchstart" in window) return null;
@ -28,7 +29,20 @@ const GSAPCursor: FC = () => {
window.addEventListener("mousemove", onMouseMove);
const handleMouseOver = () => setIsHoveringLink(true);
const handleMouseOut = () => setIsHoveringLink(false);
const links = document.querySelectorAll('a');
links.forEach(link => {
link.addEventListener('mouseover', handleMouseOver);
link.addEventListener('mouseout', handleMouseOut);
});
return () => {
links.forEach(link => {
link.removeEventListener('mouseover', handleMouseOver);
link.removeEventListener('mouseout', handleMouseOut);
});
window.removeEventListener("mousemove", onMouseMove);
};
}, []);
@ -45,7 +59,7 @@ const GSAPCursor: FC = () => {
<div>
<div
ref={cursorRef}
className="fixed w-8 h-8 rounded-full border-2 border-dotted border-red-500 bg-transparent pointer-events-none z-60"
className={`fixed w-8 h-8 ${isHoveringLink ? 'rounded-none' : 'rounded-full'} border-2 border-dotted border-red-500 bg-transparent pointer-events-none z-60`}
style={{
transform: "translate(-50%, -50%)",
}}

View file

@ -0,0 +1,95 @@
// components/MarkdownRenderer.tsx
"use client";
import React, { useEffect, useRef } from "react";
import { useTheme } from "next-themes";
import { useMarkdownRenderer } from "@/hooks/useMarkdownRenderer";
interface MarkdownRendererProps {
markdown: string;
}
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ markdown }) => {
const { theme } = useTheme();
const html = useMarkdownRenderer(markdown, theme);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const linkTags = containerRef.current.querySelectorAll("a");
linkTags.forEach((block) => {
block.setAttribute("target", "_blank");
block.setAttribute("rel", "noopener noreferrer");
});
// Find all code blocks (Shiki wraps them in <pre class="shiki">)
const codeBlocks = containerRef.current.querySelectorAll("pre.shiki");
(codeBlocks as NodeListOf<HTMLPreElement>).forEach((block) => {
// Skip if already has a copy button
if (block.querySelector(".copy-code-btn")) return;
const button = document.createElement("button");
button.className = "copy-code-btn";
button.textContent = "Copy";
button.style.position = "absolute";
button.style.right = "8px";
button.style.top = "8px";
button.style.zIndex = "10";
button.style.background = "#333";
button.style.color = "#fff";
button.style.border = "none";
button.style.borderRadius = "4px";
button.style.padding = "4px 8px";
button.style.cursor = "pointer";
button.style.fontSize = "12px";
const codeElement = block.querySelector("code");
let language = codeElement?.getAttribute("data-language") || "text";
const fileType = document.createElement("div");
fileType.className = "filetype-code-btn";
fileType.textContent = language;
fileType.style.position = "absolute";
fileType.style.left = "8px";
fileType.style.top = "8px";
fileType.style.zIndex = "10";
fileType.style.background = "#333";
fileType.style.color = "#fff";
fileType.style.border = "none";
fileType.style.borderRadius = "4px";
fileType.style.padding = "4px 8px";
fileType.style.fontSize = "12px";
button.addEventListener("click", () => {
const code = block.querySelector("code")?.textContent || "";
navigator.clipboard.writeText(code).then(() => {
button.textContent = "Copied!";
setTimeout(() => {
button.textContent = "Copy";
}, 2000);
});
});
// Make the block relative so the absolute button works
block.style.position = "relative";
block.appendChild(button);
block.appendChild(fileType);
});
}, [html]);
return (
<>
<div
ref={containerRef}
className="markdown"
dangerouslySetInnerHTML={{ __html: html }}
/>
</>
);
};
export default MarkdownRenderer;

View file

@ -0,0 +1,78 @@
"use client";
import { useEffect, useState } from "react";
import { Marked } from "marked";
import markedShiki from "marked-shiki";
import { createHighlighter, bundledLanguages } from "shiki";
import DOMPurify from "dompurify";
const darkTheme = "monokai";
const lightTheme = "rose-pine-dawn";
// You may want to parameterize theme/langs for your use case
const highlighterPromise = createHighlighter({
// TODO: can be used to import highlighting for all languages, but it slows things down considerably
langs: [
"ts",
"tsx",
"md",
"python",
"js",
"jsx",
"bash",
"json",
"rust",
"go",
"cpp",
"sql",
"ruby",
"nix",
"html",
"css",
"markdown",
"docker",
"tf",
],
themes: [lightTheme, darkTheme],
});
export function useMarkdownRenderer(
markdown: string,
theme: string | undefined,
): string {
const [html, setHtml] = useState<string>("");
useEffect(() => {
let isMounted = true;
async function renderMarkdown() {
const highlighter = await highlighterPromise;
const marked = new Marked().use(
markedShiki({
highlight(code, lang, props) {
return highlighter.codeToHtml(code, {
lang,
theme: theme === "dark" ? darkTheme : lightTheme,
meta: { __raw: props.join(" ") },
transformers: [
{
code(node) {
node.properties["data-language"] = lang;
},
},
],
});
},
}),
);
const rawHtml = await marked.parse(markdown);
const cleanHtml = DOMPurify.sanitize(rawHtml);
if (isMounted) setHtml(cleanHtml);
}
renderMarkdown();
return () => {
isMounted = false;
};
}, [markdown, theme]);
return html;
}