From e74f75ae4b8c262e9b3c0d62f00f4357578f591a Mon Sep 17 00:00:00 2001 From: Ameya Shenoy Date: Mon, 23 Jun 2025 09:22:26 +0530 Subject: [PATCH] feat: portfolio done + minimal working blog Signed-off-by: Ameya Shenoy --- frontend/package-lock.json | 575 ++++++++++++++++++ frontend/package.json | 4 + .../content.md} | 0 frontend/src/app/blog/docker_primer/page.tsx | 24 + frontend/src/app/globals.css | 105 ++++ frontend/src/app/portfolio/page.tsx | 127 ++-- .../src/components/CustomCircleCursor.tsx | 18 +- frontend/src/components/MarkdownRenderer.tsx | 95 +++ frontend/src/hooks/useMarkdownRenderer.ts | 78 +++ 9 files changed, 962 insertions(+), 64 deletions(-) rename frontend/src/app/blog/{content/docker_primer.md => docker_primer/content.md} (100%) create mode 100644 frontend/src/app/blog/docker_primer/page.tsx create mode 100644 frontend/src/components/MarkdownRenderer.tsx create mode 100644 frontend/src/hooks/useMarkdownRenderer.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bcd5994..66d4231 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 607c5b4..17f6910 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" }, diff --git a/frontend/src/app/blog/content/docker_primer.md b/frontend/src/app/blog/docker_primer/content.md similarity index 100% rename from frontend/src/app/blog/content/docker_primer.md rename to frontend/src/app/blog/docker_primer/content.md diff --git a/frontend/src/app/blog/docker_primer/page.tsx b/frontend/src/app/blog/docker_primer/page.tsx new file mode 100644 index 0000000..17b62f3 --- /dev/null +++ b/frontend/src/app/blog/docker_primer/page.tsx @@ -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 ( +
+
+
+ +
+
+
+ ); + } catch (error) { + console.error("Error loading markdown:", error); + return
Failed to load content
; + } +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index e142c10..f24c3e9 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -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) { diff --git a/frontend/src/app/portfolio/page.tsx b/frontend/src/app/portfolio/page.tsx index b9a139a..3d61b70 100644 --- a/frontend/src/app/portfolio/page.tsx +++ b/frontend/src/app/portfolio/page.tsx @@ -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 ( -
+
-
+
About Me
- -
- Download - + +
+ Resume +
-
+
-
-
Hi, I'm
+
+
Hi, I'm
Ameya Shenoy
-
-
+
+
Based in India
-
+
Principal Engineer / Engineering Lead
-
+
8+ years experience
-
+
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.
-
-
Key Skills
-
+
+
Work Experience
+ {workExperience.map((job, index) => ( +
+
+ + {job.company} + +

{job.tenure}

+
+

{job.title}

+

+ {job.summary} +

+
+ ))} +
+
+
Key Skills
+
{Object.entries(skillsLeft).map(([topic, skillsMap]) => (
-
{topic}
-
+
{topic}
+
{Object.entries(skillsMap).map(([language, icon]) => ( {language} @@ -210,12 +235,12 @@ export default async function Portfolio() {
{Object.entries(skillsRight).map(([topic, skillsMap]) => (
-
{topic}
-
+
{topic}
+
{Object.entries(skillsMap).map(([language, icon]) => ( {language} @@ -226,28 +251,6 @@ export default async function Portfolio() {
-
-
Work Experience
- {workExperience.map((job, index) => ( -
-
- - {job.company} - -

{job.tenure}

-
-

{job.title}

-

- {job.summary} -

-
- ))} -
); diff --git a/frontend/src/components/CustomCircleCursor.tsx b/frontend/src/components/CustomCircleCursor.tsx index 8d4f0c8..e5f8a0b 100644 --- a/frontend/src/components/CustomCircleCursor.tsx +++ b/frontend/src/components/CustomCircleCursor.tsx @@ -5,7 +5,8 @@ import gsap from "gsap"; const GSAPCursor: FC = () => { const cursorRef = useRef(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 = () => {
= ({ markdown }) => { + const { theme } = useTheme(); + + const html = useMarkdownRenderer(markdown, theme); + const containerRef = useRef(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
)
+    const codeBlocks = containerRef.current.querySelectorAll("pre.shiki");
+
+    (codeBlocks as NodeListOf).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 (
+    <>
+      
+ + ); +}; + +export default MarkdownRenderer; diff --git a/frontend/src/hooks/useMarkdownRenderer.ts b/frontend/src/hooks/useMarkdownRenderer.ts new file mode 100644 index 0000000..73bee23 --- /dev/null +++ b/frontend/src/hooks/useMarkdownRenderer.ts @@ -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(""); + + 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; +}