diff --git a/astro.config.mjs b/astro.config.mjs index 4a409f4..37ae6e9 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -2,6 +2,28 @@ import { defineConfig } from 'astro/config'; import mdx from '@astrojs/mdx'; import sitemap from '@astrojs/sitemap'; import tailwind from '@astrojs/tailwind'; +import remarkDirective from 'remark-directive'; +import { visit } from 'unist-util-visit'; +import { h } from 'hastscript'; + +// Custom remark plugin to handle admonitions (:::tip:::, :::warning:::, etc.) +function remarkAdmonitions() { + return (tree) => { + visit(tree, (node) => { + if ( + node.type === 'textDirective' || + node.type === 'leafDirective' || + node.type === 'containerDirective' + ) { + const data = node.data || (node.data = {}); + const tagName = node.type === 'textDirective' ? 'span' : 'div'; + + data.hName = tagName; + data.hProperties = h(tagName, { class: `admonition admonition-${node.name}` }).properties; + } + }); + }; +} // https://astro.build/config export default defineConfig({ @@ -21,13 +43,22 @@ export default defineConfig({ }), ], markdown: { + remarkPlugins: [remarkDirective, remarkAdmonitions], shikiConfig: { - theme: 'github-dark', themes: { light: 'github-light', - dark: 'github-dark', + dark: 'github-dark-dimmed', }, wrap: true, + transformers: [ + { + name: 'add-copy-button', + pre(node) { + // Add a data attribute to enable copy button + this.addClassToHast(node, 'code-block-wrapper'); + }, + }, + ], }, }, }); diff --git a/package-lock.json b/package-lock.json index 4af4ea4..ac0323c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,13 @@ "@astrojs/sitemap": "^3.1.6", "@astrojs/tailwind": "^5.1.1", "astro": "^4.15.11", + "hastscript": "^9.0.1", + "remark-admonitions": "^1.2.1", + "remark-directive": "^4.0.0", "simple-icons": "^15.20.0", "tailwindcss": "^3.4.1", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "unist-util-visit": "^5.0.0" }, "devDependencies": { "@tailwindcss/typography": "^0.5.15", @@ -3979,6 +3983,29 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -4434,6 +4461,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -4799,6 +4847,25 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", @@ -6456,6 +6523,284 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-admonitions": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/remark-admonitions/-/remark-admonitions-1.2.1.tgz", + "integrity": "sha512-Ji6p68VDvD+H1oS95Fdx9Ar5WA2wcDA4kwrrhVU7fGctC6+d3uiMICu7w7/2Xld+lnU7/gi+432+rRbup5S8ow==", + "license": "MIT", + "dependencies": { + "rehype-parse": "^6.0.2", + "unified": "^8.4.2", + "unist-util-visit": "^2.0.1" + } + }, + "node_modules/remark-admonitions/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/remark-admonitions/node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-admonitions/node_modules/ccount": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", + "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-admonitions/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-admonitions/node_modules/hast-util-from-parse5": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz", + "integrity": "sha512-gOc8UB99F6eWVWFtM9jUikjN7QkWxB3nY0df5Z0Zq1/Nkwl5V4hAAsl0tmwlgWl/1shlTF8DnNYLO8X6wRV9pA==", + "license": "MIT", + "dependencies": { + "ccount": "^1.0.3", + "hastscript": "^5.0.0", + "property-information": "^5.0.0", + "web-namespaces": "^1.1.2", + "xtend": "^4.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/hastscript": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz", + "integrity": "sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==", + "license": "MIT", + "dependencies": { + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/remark-admonitions/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "license": "MIT" + }, + "node_modules/remark-admonitions/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-admonitions/node_modules/rehype-parse": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-6.0.2.tgz", + "integrity": "sha512-0S3CpvpTAgGmnz8kiCyFLGuW5yA4OQhyNTm/nwPopZ7+PI11WnGl1TTWTGv/2hPEe/g2jRLlhVVSsoDH8waRug==", + "license": "MIT", + "dependencies": { + "hast-util-from-parse5": "^5.0.0", + "parse5": "^5.0.0", + "xtend": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-admonitions/node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-admonitions/node_modules/unified": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-8.4.2.tgz", + "integrity": "sha512-JCrmN13jI4+h9UAyKEoGcDZV+i1E7BLFuG7OsaDvTXI5P0qhHX+vZO/kOhz9jn8HGENDKbwSeB0nVOg4gVStGA==", + "license": "MIT", + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-admonitions/node_modules/web-namespaces": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz", + "integrity": "sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", + "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -8157,6 +8502,15 @@ "node": ">=8" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/package.json b/package.json index d47c984..01c2db1 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,13 @@ "@astrojs/sitemap": "^3.1.6", "@astrojs/tailwind": "^5.1.1", "astro": "^4.15.11", + "hastscript": "^9.0.1", + "remark-admonitions": "^1.2.1", + "remark-directive": "^4.0.0", "simple-icons": "^15.20.0", "tailwindcss": "^3.4.1", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "unist-util-visit": "^5.0.0" }, "devDependencies": { "@tailwindcss/typography": "^0.5.15", diff --git a/src/components/FlagIcon.astro b/src/components/FlagIcon.astro new file mode 100644 index 0000000..9e22c62 --- /dev/null +++ b/src/components/FlagIcon.astro @@ -0,0 +1,42 @@ +--- +interface Props { + country: 'us' | 'vn' | 'jp'; + class?: string; +} + +const { country, class: className = 'w-5 h-5' } = Astro.props; +--- + +{country === 'us' && ( + + + + + + + + + + + + + + + + + +)} + +{country === 'vn' && ( + + + + +)} + +{country === 'jp' && ( + + + + +)} diff --git a/src/components/LanguageBadge.astro b/src/components/LanguageBadge.astro index 468ff1d..36c732e 100644 --- a/src/components/LanguageBadge.astro +++ b/src/components/LanguageBadge.astro @@ -1,15 +1,17 @@ --- -import { getLanguageInfo } from '@utils/helpers'; +import FlagIcon from './FlagIcon.astro'; interface Props { language: 'en' | 'vi'; } const { language } = Astro.props; -const info = getLanguageInfo(language); +const country = language === 'en' ? 'us' : 'vn'; +const label = language === 'en' ? 'English' : 'Tiếng Việt'; +const badgeClass = language === 'en' ? 'lang-badge-en' : 'lang-badge-vi'; --- - - {info.flag} - {info.label === 'English' ? 'EN' : 'VI'} + + + {label} diff --git a/src/components/LanguageSwitcher.astro b/src/components/LanguageSwitcher.astro index 4e19f41..758b7ae 100644 --- a/src/components/LanguageSwitcher.astro +++ b/src/components/LanguageSwitcher.astro @@ -12,15 +12,12 @@ const alternateUrl = getLocalizedPath(currentPath, alternateLang);
- - - - {currentLang === 'en' ? 'VI' : 'EN'} + {alternateLang.toUpperCase()}
diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 3778015..8a5306b 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -27,7 +27,7 @@ export default { title: 'About Me', subtitle: 'AI Engineer with Master\'s degree from JAIST, specializing in production ML systems', greeting: 'Hello there!', - bio1: 'I\'m Hieu Nguyen, an AI Engineer from Vietnam 🇻🇳 with a passion for transforming research into production-ready systems.', + bio1: 'I\'m Hieu Nguyen, an AI Engineer from Vietnam with a passion for transforming research into production-ready systems.', bio2: 'My expertise lies in Natural Language Processing, RAG systems, and Large Language Models. I specialize in building scalable AI solutions that solve real-world problems.', bio3: 'I hold a {degree} from {school} in Japan, where I deepened my knowledge in information retrieval and machine learning.', bio4: 'When I\'m not coding, you\'ll find me exploring self-hosted solutions, contributing to open source, or writing about AI/ML on my blog.', @@ -87,7 +87,7 @@ export default { quickInfo: { title: 'Quick Info', location: 'Location', - locationValue: 'Hanoi, Vietnam 🇻🇳', + locationValue: 'Hanoi, Vietnam', role: 'Role', roleValue: 'AI Engineer', education: 'Education', @@ -124,8 +124,8 @@ export default { readArticle: 'Read Article', emptyState: 'No posts found', clearFilters: 'Clear filters', - english: '🇺🇸 English', - vietnamese: '🇻🇳 Vietnamese', + english: 'English', + vietnamese: 'Vietnamese', filter: { all: 'All', aiml: 'AI/ML', diff --git a/src/i18n/vi.ts b/src/i18n/vi.ts index 2b26145..f64192e 100644 --- a/src/i18n/vi.ts +++ b/src/i18n/vi.ts @@ -27,7 +27,7 @@ export default { title: 'Về Tôi', subtitle: 'Kỹ sư AI với bằng Thạc sĩ từ JAIST, chuyên về hệ thống ML production', greeting: 'Xin chào!', - bio1: 'Tôi là Hiếu Nguyễn, một Kỹ sư AI từ Việt Nam 🇻🇳 với đam mê chuyển đổi nghiên cứu thành các hệ thống production.', + bio1: 'Tôi là Hiếu Nguyễn, một Kỹ sư AI từ Việt Nam với đam mê chuyển đổi nghiên cứu thành các hệ thống production.', bio2: 'Chuyên môn của tôi nằm ở Xử lý Ngôn ngữ Tự nhiên, hệ thống RAG và Mô hình Ngôn ngữ Lớn. Tôi chuyên xây dựng các giải pháp AI có khả năng mở rộng để giải quyết các vấn đề thực tế.', bio3: 'Tôi có bằng {degree} từ {school} ở Nhật Bản, nơi tôi đã nâng cao kiến thức về truy xuất thông tin và học máy.', bio4: 'Khi không code, bạn sẽ thấy tôi khám phá các giải pháp self-hosted, đóng góp cho open source, hoặc viết về AI/ML trên blog.', @@ -87,7 +87,7 @@ export default { quickInfo: { title: 'Thông Tin Nhanh', location: 'Vị Trí', - locationValue: 'Hà Nội, Việt Nam 🇻🇳', + locationValue: 'Hà Nội, Việt Nam', role: 'Vai Trò', roleValue: 'Kỹ Sư AI', education: 'Học Vấn', @@ -124,8 +124,8 @@ export default { readArticle: 'Đọc Bài Viết', emptyState: 'Không tìm thấy bài viết', clearFilters: 'Xóa bộ lọc', - english: '🇺🇸 English', - vietnamese: '🇻🇳 Tiếng Việt', + english: 'English', + vietnamese: 'Tiếng Việt', filter: { all: 'Tất Cả', aiml: 'AI/ML', diff --git a/src/layouts/BlogPostLayout.astro b/src/layouts/BlogPostLayout.astro index 3288022..9989bbf 100644 --- a/src/layouts/BlogPostLayout.astro +++ b/src/layouts/BlogPostLayout.astro @@ -3,6 +3,7 @@ import BaseLayout from './BaseLayout.astro'; import TableOfContents from '@components/TableOfContents.astro'; import Giscus from '@components/Giscus.astro'; import LanguageBadge from '@components/LanguageBadge.astro'; +import SocialIcon from '@components/SocialIcon.astro'; import { formatDate, getCategoryColor } from '@utils/helpers'; import type { CollectionEntry } from 'astro:content'; @@ -102,23 +103,31 @@ const { title, description, date, updated, author, category, tags, language } = href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(Astro.url.href)}`} target="_blank" rel="noopener noreferrer" - class="btn-secondary text-sm" + class="btn-secondary text-sm flex items-center gap-2" + aria-label="Share on Twitter" > - 🐦 Twitter + + Twitter - 💼 LinkedIn + + LinkedIn @@ -155,15 +164,51 @@ const { title, description, date, updated, author, category, tags, language } = document.getElementById('copy-link')?.addEventListener('click', async () => { await navigator.clipboard.writeText(window.location.href); const button = document.getElementById('copy-link'); - const originalText = button?.textContent || ''; - if (button) { - button.textContent = '✓ Copied!'; + const span = button?.querySelector('span'); + if (span) { + const originalText = span.textContent || ''; + span.textContent = '✓ Copied!'; setTimeout(() => { - button.textContent = originalText; + span.textContent = originalText; }, 2000); } }); + // Add copy buttons to code blocks + document.querySelectorAll('.prose pre').forEach((pre) => { + // Create copy button + const button = document.createElement('button'); + button.className = 'copy-button'; + button.textContent = 'Copy'; + button.setAttribute('aria-label', 'Copy code to clipboard'); + + // Add click handler + button.addEventListener('click', async () => { + const code = pre.querySelector('code'); + if (code) { + const text = code.textContent || ''; + try { + await navigator.clipboard.writeText(text); + button.textContent = 'Copied!'; + button.classList.add('copied'); + setTimeout(() => { + button.textContent = 'Copy'; + button.classList.remove('copied'); + }, 2000); + } catch (err) { + console.error('Failed to copy code:', err); + button.textContent = 'Failed'; + setTimeout(() => { + button.textContent = 'Copy'; + }, 2000); + } + } + }); + + // Append button to pre element + pre.appendChild(button); + }); + // Smooth scroll for anchor links document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener('click', function (this: HTMLAnchorElement, e: Event) { diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 2df4fb3..05f0c19 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -2,6 +2,7 @@ import { getCollection } from 'astro:content'; import BaseLayout from '@layouts/BaseLayout.astro'; import BlogCard from '@components/BlogCard.astro'; +import FlagIcon from '@components/FlagIcon.astro'; import { readingTime } from '@utils/helpers'; import { getLangFromUrl, useTranslations } from '@i18n/utils'; @@ -87,7 +88,8 @@ const featuredPost = sortedPosts[0];

- {featuredPost.data.language === 'vi' ? '🇻🇳 VI' : '🇺🇸 EN'} + + {featuredPost.data.language === 'vi' ? 'VI' : 'EN'} {featuredPost.data.category} diff --git a/src/pages/vi/blog/index.astro b/src/pages/vi/blog/index.astro index 2df4fb3..05f0c19 100644 --- a/src/pages/vi/blog/index.astro +++ b/src/pages/vi/blog/index.astro @@ -2,6 +2,7 @@ import { getCollection } from 'astro:content'; import BaseLayout from '@layouts/BaseLayout.astro'; import BlogCard from '@components/BlogCard.astro'; +import FlagIcon from '@components/FlagIcon.astro'; import { readingTime } from '@utils/helpers'; import { getLangFromUrl, useTranslations } from '@i18n/utils'; @@ -87,7 +88,8 @@ const featuredPost = sortedPosts[0];

- {featuredPost.data.language === 'vi' ? '🇻🇳 VI' : '🇺🇸 EN'} + + {featuredPost.data.language === 'vi' ? 'VI' : 'EN'} {featuredPost.data.category} diff --git a/src/styles/global.css b/src/styles/global.css index bee62b1..f83d1c5 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -133,11 +133,34 @@ } .prose pre { - @apply bg-slate-900 dark:bg-slate-950 rounded-lg p-4 overflow-x-auto my-6; + @apply bg-slate-100 dark:bg-slate-900 rounded-lg p-4 overflow-x-auto my-6 relative; + position: relative; } .prose pre code { - @apply bg-transparent text-slate-100 p-0; + @apply bg-transparent p-0; +} + +/* Code block wrapper with copy button */ +.prose .code-block-wrapper { + @apply relative; +} + +.prose pre:hover .copy-button { + @apply opacity-100; +} + +.prose .copy-button { + @apply absolute top-2 right-2 px-3 py-1.5 bg-slate-700 hover:bg-slate-600 dark:bg-slate-600 dark:hover:bg-slate-500 text-white text-xs rounded opacity-0 transition-all duration-200 cursor-pointer font-sans; + z-index: 10; +} + +.prose .copy-button:hover { + @apply shadow-lg; +} + +.prose .copy-button.copied { + @apply bg-green-600 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-600; } .prose blockquote { @@ -187,6 +210,56 @@ @apply bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300; } +/* Admonitions (:::tip:::, :::warning:::, etc.) */ +.prose .admonition { + @apply my-6 p-4 rounded-lg border-l-4; +} + +.prose .admonition-tip { + @apply bg-blue-50 dark:bg-blue-950 border-blue-500 text-blue-900 dark:text-blue-100; +} + +.prose .admonition-tip::before { + content: '💡 Tip'; + @apply block font-bold mb-2 text-blue-700 dark:text-blue-300; +} + +.prose .admonition-warning { + @apply bg-yellow-50 dark:bg-yellow-950 border-yellow-500 text-yellow-900 dark:text-yellow-100; +} + +.prose .admonition-warning::before { + content: '⚠️ Warning'; + @apply block font-bold mb-2 text-yellow-700 dark:text-yellow-300; +} + +.prose .admonition-danger { + @apply bg-red-50 dark:bg-red-950 border-red-500 text-red-900 dark:text-red-100; +} + +.prose .admonition-danger::before { + content: '🚨 Danger'; + @apply block font-bold mb-2 text-red-700 dark:text-red-300; +} + +.prose .admonition-note { + @apply bg-gray-50 dark:bg-gray-950 border-gray-500 text-gray-900 dark:text-gray-100; +} + +.prose .admonition-note::before { + content: '📝 Note'; + @apply block font-bold mb-2 text-gray-700 dark:text-gray-300; +} + +.prose .admonition-info { + @apply bg-cyan-50 dark:bg-cyan-950 border-cyan-500 text-cyan-900 dark:text-cyan-100; +} + +.prose .admonition-info::before { + content: 'ℹ️ Info'; + @apply block font-bold mb-2 text-cyan-700 dark:text-cyan-300; +} + /* Category badges */ .category-badge { @apply inline-block px-3 py-1 text-sm font-medium rounded-full transition-colors; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 4596b8c..aa76deb 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,7 +4,7 @@ export const SITE = { url: 'https://behitek.com', author: 'Hieu Nguyen', email: 'hello@behitek.com', - tagline: 'AI Engineer • Vietnam 🇻🇳', + tagline: 'AI Engineer • Vietnam', bio: 'I love bringing AI into production to solve real problems, with a focus on natural language processing, retrieval augmented generation (RAG), LLMs, and information retrieval.', }; @@ -40,7 +40,7 @@ export const NAV_LINKS = [ export const EDUCATION = { degree: "Master's in Information Science", school: 'Japan Advanced Institute of Science and Technology (JAIST)', - country: '🇯🇵 Japan', + country: 'Japan', }; export const TECH_STACK = {