diff --git a/package-lock.json b/package-lock.json index 27425e9..9329769 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.14.1", + "react-syntax-highlighter": "^16.1.1", "shadcn": "^4.2.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", @@ -28,6 +29,7 @@ "@types/node": "^24.12.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^5.2.0", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", @@ -432,6 +434,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -3836,6 +3847,15 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "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/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3853,6 +3873,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -3873,12 +3899,28 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/statuses": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "license": "MIT" }, + "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/@types/validate-npm-package-name": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", @@ -4557,6 +4599,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "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/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4710,6 +4782,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "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": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", @@ -4874,6 +4956,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -5564,6 +5659,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5703,6 +5811,14 @@ "dev": true, "license": "ISC" }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -5967,6 +6083,36 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/headers-polyfill": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", @@ -5990,6 +6136,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/hono": { "version": "4.12.14", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", @@ -6116,12 +6277,46 @@ "node": ">= 0.10" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -6167,6 +6362,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-in-ssh": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", @@ -6767,6 +6972,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7299,6 +7518,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/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/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -7569,6 +7813,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -7591,6 +7844,16 @@ "node": ">=6" } }, + "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/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7901,6 +8164,26 @@ } } }, + "node_modules/react-syntax-highlighter": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/recast": { "version": "0.23.11", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", @@ -7917,6 +8200,22 @@ "node": ">= 4" } }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8352,6 +8651,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/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index c828cce..7b0b6c7 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-router-dom": "^7.14.1", + "react-syntax-highlighter": "^16.1.1", "shadcn": "^4.2.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", @@ -32,6 +33,7 @@ "@types/node": "^24.12.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^5.2.0", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", diff --git a/src/App.tsx b/src/App.tsx index 68bbbc5..9e25606 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { SidebarTrigger, } from "@/components/ui/sidebar" import { Base64Page } from "@/pages/base64" +import { GistPage } from "@/pages/gist" import { HomePage } from "@/pages/home" import { TimestampPage } from "@/pages/timestamp" @@ -26,6 +27,7 @@ export function App() { } /> } /> } /> + } /> diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index dd143b4..e879e97 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -24,6 +24,10 @@ const navItems = [ title: "Date & Time", items: [{ title: "Timestamp", url: "/timestamp" }], }, + { + title: "Storage", + items: [{ title: "Gist", url: "/gist" }], + }, ] export function AppSidebar({ ...props }: React.ComponentProps) { diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..0905b69 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,38 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-none border px-1.5 py-0.5 text-[10px] font-medium transition-colors", + { + variants: { + variant: { + default: + "border-transparent bg-primary/10 text-primary", + secondary: + "border-transparent bg-secondary text-secondary-foreground", + outline: "border-border text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + ...props +}: React.ComponentProps<"span"> & VariantProps) { + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..cfb419f --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..85deba6 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,140 @@ +import * as React from "react" +import { Dialog as DialogPrimitive } from "radix-ui" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..8614b98 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from "react" +import { Label as LabelPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..a70bc60 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,179 @@ +import * as React from "react" +import { Select as SelectPrimitive } from "radix-ui" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + span]:line-clamp-1 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", + className + )} + {...props} + > + {children} + + + + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/src/hooks/use-gists.ts b/src/hooks/use-gists.ts new file mode 100644 index 0000000..3ab86e6 --- /dev/null +++ b/src/hooks/use-gists.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useState } from "react" + +import type { Gist } from "@/types/gist" + +const STORAGE_KEY = "dev-tools-gists" + +function loadGists(): Gist[] { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + return JSON.parse(raw) as Gist[] + } catch { + return [] + } +} + +function saveGists(gists: Gist[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(gists)) +} + +export function useGists() { + const [gists, setGists] = useState(loadGists) + + useEffect(() => { + saveGists(gists) + }, [gists]) + + const createGist = useCallback( + (data: { title: string; language: string; content: string }) => { + const now = Date.now() + const gist: Gist = { + id: crypto.randomUUID(), + title: data.title, + language: data.language, + content: data.content, + createdAt: now, + updatedAt: now, + } + setGists((prev) => [gist, ...prev]) + return gist + }, + [] + ) + + const updateGist = useCallback( + (id: string, data: { title: string; language: string; content: string }) => { + setGists((prev) => + prev.map((g) => + g.id === id + ? { ...g, ...data, updatedAt: Date.now() } + : g + ) + ) + }, + [] + ) + + const deleteGist = useCallback((id: string) => { + setGists((prev) => prev.filter((g) => g.id !== id)) + }, []) + + const deleteGists = useCallback((ids: string[]) => { + const idSet = new Set(ids) + setGists((prev) => prev.filter((g) => !idSet.has(g.id))) + }, []) + + return { gists, createGist, updateGist, deleteGist, deleteGists } +} diff --git a/src/pages/gist.tsx b/src/pages/gist.tsx new file mode 100644 index 0000000..c5dc242 --- /dev/null +++ b/src/pages/gist.tsx @@ -0,0 +1,427 @@ +import { useMemo, useState } from "react" +import { toast } from "sonner" +import { + CopyIcon, + PencilIcon, + PlusIcon, + SearchIcon, + Trash2Icon, +} from "lucide-react" +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" +import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { useGists } from "@/hooks/use-gists" +import type { Gist } from "@/types/gist" + +const LANGUAGES = [ + { value: "plaintext", label: "Plain Text" }, + { value: "javascript", label: "JavaScript" }, + { value: "typescript", label: "TypeScript" }, + { value: "jsx", label: "JSX" }, + { value: "tsx", label: "TSX" }, + { value: "python", label: "Python" }, + { value: "java", label: "Java" }, + { value: "c", label: "C" }, + { value: "cpp", label: "C++" }, + { value: "csharp", label: "C#" }, + { value: "go", label: "Go" }, + { value: "rust", label: "Rust" }, + { value: "ruby", label: "Ruby" }, + { value: "php", label: "PHP" }, + { value: "swift", label: "Swift" }, + { value: "kotlin", label: "Kotlin" }, + { value: "css", label: "CSS" }, + { value: "html", label: "HTML" }, + { value: "sql", label: "SQL" }, + { value: "bash", label: "Bash/Shell" }, + { value: "json", label: "JSON" }, + { value: "yaml", label: "YAML" }, + { value: "markdown", label: "Markdown" }, +] + +function getLanguageLabel(value: string) { + return LANGUAGES.find((l) => l.value === value)?.label ?? value +} + +type FormState = { + title: string + language: string + content: string +} + +const DEFAULT_FORM: FormState = { + title: "", + language: "plaintext", + content: "", +} + +export function GistPage() { + const { gists, createGist, updateGist, deleteGist, deleteGists } = useGists() + const [search, setSearch] = useState("") + const [selected, setSelected] = useState>(new Set()) + const [dialogOpen, setDialogOpen] = useState(false) + const [editTarget, setEditTarget] = useState(null) + const [form, setForm] = useState(DEFAULT_FORM) + const [deleteConfirmIds, setDeleteConfirmIds] = useState(null) + + const filtered = useMemo(() => { + const q = search.toLowerCase() + if (!q) return gists + return gists.filter( + (g) => + g.title.toLowerCase().includes(q) || + g.content.toLowerCase().includes(q) || + g.language.toLowerCase().includes(q) + ) + }, [gists, search]) + + const allFilteredSelected = + filtered.length > 0 && filtered.every((g) => selected.has(g.id)) + + function openCreate() { + setEditTarget(null) + setForm(DEFAULT_FORM) + setDialogOpen(true) + } + + function openEdit(gist: Gist) { + setEditTarget(gist) + setForm({ title: gist.title, language: gist.language, content: gist.content }) + setDialogOpen(true) + } + + function handleSave() { + if (!form.content.trim()) { + toast.error("Content is required.") + return + } + if (editTarget) { + updateGist(editTarget.id, form) + toast.success("Gist updated.") + } else { + createGist(form) + toast.success("Gist created.") + } + setDialogOpen(false) + } + + function handleDelete(ids: string[]) { + setDeleteConfirmIds(ids) + } + + function confirmDelete() { + if (!deleteConfirmIds) return + if (deleteConfirmIds.length === 1) { + deleteGist(deleteConfirmIds[0]) + } else { + deleteGists(deleteConfirmIds) + } + setSelected((prev) => { + const next = new Set(prev) + deleteConfirmIds.forEach((id) => next.delete(id)) + return next + }) + toast.success( + deleteConfirmIds.length === 1 ? "Gist deleted." : `${deleteConfirmIds.length} gists deleted.` + ) + setDeleteConfirmIds(null) + } + + function toggleSelect(id: string) { + setSelected((prev) => { + const next = new Set(prev) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } + + function toggleSelectAll() { + if (allFilteredSelected) { + setSelected((prev) => { + const next = new Set(prev) + filtered.forEach((g) => next.delete(g.id)) + return next + }) + } else { + setSelected((prev) => { + const next = new Set(prev) + filtered.forEach((g) => next.add(g.id)) + return next + }) + } + } + + function handleCopy(content: string) { + navigator.clipboard.writeText(content).then( + () => toast.success("Copied to clipboard."), + () => toast.error("Failed to copy to clipboard.") + ) + } + + const selectedInView = filtered.filter((g) => selected.has(g.id)) + + return ( +
+
+
+

Gist

+

+ Save and manage code snippets with syntax highlighting. +

+
+ +
+ +
+
+ + setSearch(e.target.value)} + /> +
+ {selectedInView.length > 0 && ( + + )} +
+ + {filtered.length === 0 ? ( +
+ {gists.length === 0 ? ( + <> +

No gists yet

+

Click "New Gist" to create your first snippet.

+ + ) : ( + <> +

No results

+

Try a different search term.

+ + )} +
+ ) : ( +
+
+ + + {filtered.length} snippet{filtered.length !== 1 ? "s" : ""} + +
+ {filtered.map((gist) => ( + handleDelete([id])} + onCopy={handleCopy} + /> + ))} +
+ )} + + {/* Create / Edit dialog */} + + + + {editTarget ? "Edit Gist" : "New Gist"} + +
+
+ + setForm((f) => ({ ...f, title: e.target.value }))} + /> +
+
+ + +
+
+ +