feat: add markdown rendering support and enhance file preview component
This commit is contained in:
		
							parent
							
								
									036f23b777
								
							
						
					
					
						commit
						60925ee6ac
					
				
							
								
								
									
										195
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										195
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -18,6 +18,8 @@ | |||||||
|         "@trpc/react-query": "^11.0.0", |         "@trpc/react-query": "^11.0.0", | ||||||
|         "@trpc/server": "^11.0.0", |         "@trpc/server": "^11.0.0", | ||||||
|         "dompurify": "^3.2.5", |         "dompurify": "^3.2.5", | ||||||
|  |         "github-markdown-css": "^5.8.1", | ||||||
|  |         "gray-matter": "^4.0.3", | ||||||
|         "mermaid": "^11.6.0", |         "mermaid": "^11.6.0", | ||||||
|         "minio": "^8.0.5", |         "minio": "^8.0.5", | ||||||
|         "next": "^15.2.3", |         "next": "^15.2.3", | ||||||
| @ -27,7 +29,9 @@ | |||||||
|         "react-hot-toast": "^2.5.2", |         "react-hot-toast": "^2.5.2", | ||||||
|         "react-markdown": "^10.1.0", |         "react-markdown": "^10.1.0", | ||||||
|         "rehype-raw": "^7.0.0", |         "rehype-raw": "^7.0.0", | ||||||
|  |         "remark": "^15.0.1", | ||||||
|         "remark-gfm": "^4.0.1", |         "remark-gfm": "^4.0.1", | ||||||
|  |         "remark-html": "^16.0.1", | ||||||
|         "server-only": "^0.0.1", |         "server-only": "^0.0.1", | ||||||
|         "superjson": "^2.2.1", |         "superjson": "^2.2.1", | ||||||
|         "zod": "^3.24.2" |         "zod": "^3.24.2" | ||||||
| @ -4883,6 +4887,19 @@ | |||||||
|         "url": "https://opencollective.com/eslint" |         "url": "https://opencollective.com/eslint" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/esprima": { | ||||||
|  |       "version": "4.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", | ||||||
|  |       "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", | ||||||
|  |       "license": "BSD-2-Clause", | ||||||
|  |       "bin": { | ||||||
|  |         "esparse": "bin/esparse.js", | ||||||
|  |         "esvalidate": "bin/esvalidate.js" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=4" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/esquery": { |     "node_modules/esquery": { | ||||||
|       "version": "1.6.0", |       "version": "1.6.0", | ||||||
|       "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", |       "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", | ||||||
| @ -4957,6 +4974,18 @@ | |||||||
|       "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", |       "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/extend-shallow": { | ||||||
|  |       "version": "2.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", | ||||||
|  |       "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "is-extendable": "^0.1.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/fast-deep-equal": { |     "node_modules/fast-deep-equal": { | ||||||
|       "version": "3.1.3", |       "version": "3.1.3", | ||||||
|       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", |       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", | ||||||
| @ -5247,6 +5276,18 @@ | |||||||
|         "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" |         "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/github-markdown-css": { | ||||||
|  |       "version": "5.8.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz", | ||||||
|  |       "integrity": "sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=10" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/glob-parent": { |     "node_modules/glob-parent": { | ||||||
|       "version": "6.0.2", |       "version": "6.0.2", | ||||||
|       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", |       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", | ||||||
| @ -5325,6 +5366,43 @@ | |||||||
|       "dev": true, |       "dev": true, | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/gray-matter": { | ||||||
|  |       "version": "4.0.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", | ||||||
|  |       "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "js-yaml": "^3.13.1", | ||||||
|  |         "kind-of": "^6.0.2", | ||||||
|  |         "section-matter": "^1.0.0", | ||||||
|  |         "strip-bom-string": "^1.0.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=6.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/gray-matter/node_modules/argparse": { | ||||||
|  |       "version": "1.0.10", | ||||||
|  |       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", | ||||||
|  |       "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "sprintf-js": "~1.0.2" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/gray-matter/node_modules/js-yaml": { | ||||||
|  |       "version": "3.14.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", | ||||||
|  |       "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "argparse": "^1.0.7", | ||||||
|  |         "esprima": "^4.0.0" | ||||||
|  |       }, | ||||||
|  |       "bin": { | ||||||
|  |         "js-yaml": "bin/js-yaml.js" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/hachure-fill": { |     "node_modules/hachure-fill": { | ||||||
|       "version": "0.5.2", |       "version": "0.5.2", | ||||||
|       "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", |       "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", | ||||||
| @ -5479,6 +5557,44 @@ | |||||||
|         "url": "https://opencollective.com/unified" |         "url": "https://opencollective.com/unified" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/hast-util-sanitize": { | ||||||
|  |       "version": "5.0.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", | ||||||
|  |       "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@types/hast": "^3.0.0", | ||||||
|  |         "@ungap/structured-clone": "^1.0.0", | ||||||
|  |         "unist-util-position": "^5.0.0" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "opencollective", | ||||||
|  |         "url": "https://opencollective.com/unified" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "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-to-jsx-runtime": { |     "node_modules/hast-util-to-jsx-runtime": { | ||||||
|       "version": "2.3.6", |       "version": "2.3.6", | ||||||
|       "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", |       "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", | ||||||
| @ -5880,6 +5996,15 @@ | |||||||
|         "url": "https://github.com/sponsors/wooorm" |         "url": "https://github.com/sponsors/wooorm" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/is-extendable": { | ||||||
|  |       "version": "0.1.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", | ||||||
|  |       "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/is-extglob": { |     "node_modules/is-extglob": { | ||||||
|       "version": "2.1.1", |       "version": "2.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", |       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", | ||||||
| @ -6315,6 +6440,15 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", |       "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", | ||||||
|       "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" |       "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/kind-of": { | ||||||
|  |       "version": "6.0.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", | ||||||
|  |       "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/kolorist": { |     "node_modules/kolorist": { | ||||||
|       "version": "1.8.0", |       "version": "1.8.0", | ||||||
|       "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", |       "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", | ||||||
| @ -8716,6 +8850,22 @@ | |||||||
|         "url": "https://opencollective.com/unified" |         "url": "https://opencollective.com/unified" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/remark": { | ||||||
|  |       "version": "15.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", | ||||||
|  |       "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@types/mdast": "^4.0.0", | ||||||
|  |         "remark-parse": "^11.0.0", | ||||||
|  |         "remark-stringify": "^11.0.0", | ||||||
|  |         "unified": "^11.0.0" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "opencollective", | ||||||
|  |         "url": "https://opencollective.com/unified" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/remark-gfm": { |     "node_modules/remark-gfm": { | ||||||
|       "version": "4.0.1", |       "version": "4.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", |       "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", | ||||||
| @ -8734,6 +8884,23 @@ | |||||||
|         "url": "https://opencollective.com/unified" |         "url": "https://opencollective.com/unified" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/remark-html": { | ||||||
|  |       "version": "16.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/remark-html/-/remark-html-16.0.1.tgz", | ||||||
|  |       "integrity": "sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@types/mdast": "^4.0.0", | ||||||
|  |         "hast-util-sanitize": "^5.0.0", | ||||||
|  |         "hast-util-to-html": "^9.0.0", | ||||||
|  |         "mdast-util-to-hast": "^13.0.0", | ||||||
|  |         "unified": "^11.0.0" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "opencollective", | ||||||
|  |         "url": "https://opencollective.com/unified" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/remark-parse": { |     "node_modules/remark-parse": { | ||||||
|       "version": "11.0.0", |       "version": "11.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", |       "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", | ||||||
| @ -8974,6 +9141,19 @@ | |||||||
|       "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", |       "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/section-matter": { | ||||||
|  |       "version": "1.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", | ||||||
|  |       "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "dependencies": { | ||||||
|  |         "extend-shallow": "^2.0.1", | ||||||
|  |         "kind-of": "^6.0.0" | ||||||
|  |       }, | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=4" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/semver": { |     "node_modules/semver": { | ||||||
|       "version": "7.7.1", |       "version": "7.7.1", | ||||||
|       "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", |       "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", | ||||||
| @ -9219,6 +9399,12 @@ | |||||||
|         "node": ">=6" |         "node": ">=6" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/sprintf-js": { | ||||||
|  |       "version": "1.0.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", | ||||||
|  |       "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", | ||||||
|  |       "license": "BSD-3-Clause" | ||||||
|  |     }, | ||||||
|     "node_modules/stable-hash": { |     "node_modules/stable-hash": { | ||||||
|       "version": "0.0.5", |       "version": "0.0.5", | ||||||
|       "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", |       "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", | ||||||
| @ -9404,6 +9590,15 @@ | |||||||
|         "node": ">=4" |         "node": ">=4" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/strip-bom-string": { | ||||||
|  |       "version": "1.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", | ||||||
|  |       "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", | ||||||
|  |       "license": "MIT", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">=0.10.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/strip-json-comments": { |     "node_modules/strip-json-comments": { | ||||||
|       "version": "3.1.1", |       "version": "3.1.1", | ||||||
|       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", |       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", | ||||||
|  | |||||||
| @ -30,6 +30,8 @@ | |||||||
|     "@trpc/react-query": "^11.0.0", |     "@trpc/react-query": "^11.0.0", | ||||||
|     "@trpc/server": "^11.0.0", |     "@trpc/server": "^11.0.0", | ||||||
|     "dompurify": "^3.2.5", |     "dompurify": "^3.2.5", | ||||||
|  |     "github-markdown-css": "^5.8.1", | ||||||
|  |     "gray-matter": "^4.0.3", | ||||||
|     "mermaid": "^11.6.0", |     "mermaid": "^11.6.0", | ||||||
|     "minio": "^8.0.5", |     "minio": "^8.0.5", | ||||||
|     "next": "^15.2.3", |     "next": "^15.2.3", | ||||||
| @ -39,7 +41,9 @@ | |||||||
|     "react-hot-toast": "^2.5.2", |     "react-hot-toast": "^2.5.2", | ||||||
|     "react-markdown": "^10.1.0", |     "react-markdown": "^10.1.0", | ||||||
|     "rehype-raw": "^7.0.0", |     "rehype-raw": "^7.0.0", | ||||||
|  |     "remark": "^15.0.1", | ||||||
|     "remark-gfm": "^4.0.1", |     "remark-gfm": "^4.0.1", | ||||||
|  |     "remark-html": "^16.0.1", | ||||||
|     "server-only": "^0.0.1", |     "server-only": "^0.0.1", | ||||||
|     "superjson": "^2.2.1", |     "superjson": "^2.2.1", | ||||||
|     "zod": "^3.24.2" |     "zod": "^3.24.2" | ||||||
|  | |||||||
| @ -105,7 +105,7 @@ export default function FileGrid({ session }: FileGridProps) { | |||||||
|             key={file.id} |             key={file.id} | ||||||
|             className="flex place-content-end max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" |             className="flex place-content-end max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" | ||||||
|           > |           > | ||||||
|             {<div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={file.extension} /></div>} |             {<div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={file.extension} share={false} /></div>} | ||||||
| 
 | 
 | ||||||
|             <button onClick={() => router.push(pageUrl + file.url)}> |             <button onClick={() => router.push(pageUrl + file.url)}> | ||||||
|               <h3 className="text-2xl font-bold">{file.name}</h3> |               <h3 className="text-2xl font-bold">{file.name}</h3> | ||||||
|  | |||||||
| @ -1,16 +1,22 @@ | |||||||
| "use client"; | "use client"; | ||||||
| 
 | 
 | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { getFileType } from "~/utils/fileType"; // Adjust the import path as necessary
 | import { remark } from 'remark'; | ||||||
|  | import html from 'remark-html'; | ||||||
|  | import matter from 'gray-matter'; | ||||||
|  | import "github-markdown-css/github-markdown.css"; | ||||||
|  | import "../styles/custom.css"; // Adjust the path as necessary
 | ||||||
|  | import { MarkdownRenderer } from "../../components/MarkdownRenderer"; | ||||||
| 
 | 
 | ||||||
| interface FilePreviewProps { | interface FilePreviewProps { | ||||||
|   fileId: string; |   fileId: string; | ||||||
|   fileType: string; // Pass the file type as a prop
 |   fileType: string; // Pass the file type as a prop
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function FilePreview({ fileId, fileType }: FilePreviewProps) { | export function FilePreview({ fileId, fileType, share }: FilePreviewProps & { share: boolean }) { | ||||||
|   const [mediaSrc, setMediaSrc] = useState<string | null>(null); |   const [mediaSrc, setMediaSrc] = useState<string | null>(null); | ||||||
|   const [error, setError] = useState<string | null>(null); |   const [error, setError] = useState<string | null>(null); | ||||||
|  |   const [markdownContent, setMarkdownContent] = useState<string | null>(null); | ||||||
| 
 | 
 | ||||||
|   console.log("File Type:", fileType); |   console.log("File Type:", fileType); | ||||||
| 
 | 
 | ||||||
| @ -47,20 +53,53 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) { | |||||||
|     }; |     }; | ||||||
|   }, [fileId]); |   }, [fileId]); | ||||||
| 
 | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (fileType.startsWith("markdown")) { | ||||||
|  |       const fetchMarkdown = async () => { | ||||||
|  |         try { | ||||||
|  |           const result = await renderMarkdown({ id: fileId }); | ||||||
|  |           setMarkdownContent(result.props.postData.contentHtml); | ||||||
|  |         } catch (err) { | ||||||
|  |           console.error("Failed to fetch markdown content:", err); | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       fetchMarkdown(); | ||||||
|  |     } | ||||||
|  |   }, [fileId, fileType]); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|   if (error) { |   if (error) { | ||||||
|     return <div className="text-red-500">{error}</div>; |     return <div className="text-red-500">{error}</div>; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (!mediaSrc) { |   if (!mediaSrc && !markdownContent) { | ||||||
|     return <div>Loading...</div>; |     return <div>Loading...</div>; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   if (fileType.startsWith("markdown")) { | ||||||
|  |     if (share) { | ||||||
|  |       return ( | ||||||
|  |         <div className="overflow-y-auto max-h-96 rounded-lg shadow-md"> | ||||||
|  |           {markdownContent ? ( | ||||||
|  |             <MarkdownRenderer markdownContent={markdownContent} /> | ||||||
|  |           ) : ( | ||||||
|  |             <div>Loading markdown...</div> | ||||||
|  |           )} | ||||||
|  |         </div> | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return ( | ||||||
|  |       <img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" /> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   if (fileType.startsWith("video")) { |   if (fileType.startsWith("video")) { | ||||||
|     return ( |     return ( | ||||||
|       <video |       <video | ||||||
|         controls |         controls | ||||||
|         className="max-w-full max-h-96 rounded-lg shadow-md" |         className="max-w-full max-h-96 rounded-lg shadow-md" | ||||||
|         src={mediaSrc} |         src={mediaSrc || ""} | ||||||
|       > |       > | ||||||
|         Your browser does not support the video tag. |         Your browser does not support the video tag. | ||||||
|       </video> |       </video> | ||||||
| @ -71,14 +110,14 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) { | |||||||
|       <audio |       <audio | ||||||
|         controls |         controls | ||||||
|         className="max-w-full max-h-96 rounded-lg shadow-md" |         className="max-w-full max-h-96 rounded-lg shadow-md" | ||||||
|         src={mediaSrc} |         src={mediaSrc || ""} | ||||||
|       > |       > | ||||||
|         Your browser does not support the audio tag. |         Your browser does not support the audio tag. | ||||||
|       </audio> |       </audio> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   if (fileType.startsWith("image")) { |   if (fileType.startsWith("image")) { | ||||||
|     return <img src={mediaSrc} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />; |     return <img src={mediaSrc || ""} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (fileType.startsWith("text")) { |   if (fileType.startsWith("text")) { | ||||||
| @ -91,17 +130,48 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) { | |||||||
|       <img src="/icons/files/archive.svg" alt="Archive file preview" className="max-w-full max-h-96 rounded-lg invert" /> |       <img src="/icons/files/archive.svg" alt="Archive file preview" className="max-w-full max-h-96 rounded-lg invert" /> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   if (fileType.startsWith("code") || fileType.startsWith("markdown")) { |   if (fileType.startsWith("code")) { | ||||||
|     return ( |     return ( | ||||||
|       <img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" /> |       <img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" /> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   // if (fileType.startsWith("markdown")) {
 |  | ||||||
|   //   return;      
 |  | ||||||
|   //   }
 |  | ||||||
| 
 | 
 | ||||||
|   // log file type
 |  | ||||||
|   console.log("Unsupported file type:", fileType); |   console.log("Unsupported file type:", fileType); | ||||||
| 
 | 
 | ||||||
|   return; |   return null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function rendererMarkdown(id: string) { | ||||||
|  |   const fileContents = await fetch(`/api/files/serv?id=${encodeURIComponent(id)}`) | ||||||
|  |     .then((res) => res.text()) | ||||||
|  |     .catch((err) => { | ||||||
|  |       console.error("Failed to fetch file contents:", err); | ||||||
|  |       return null; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |   if (!fileContents) { | ||||||
|  |     throw new Error("File contents could not be fetched."); | ||||||
|  |   } | ||||||
|  |   const matterResult = matter(fileContents); | ||||||
|  | 
 | ||||||
|  |   const processedContent = await remark() | ||||||
|  |     .use(html) | ||||||
|  |     .process(matterResult.content); | ||||||
|  |   const contentHtml = processedContent.toString(); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     id, | ||||||
|  |     contentHtml, | ||||||
|  |     ...matterResult.data, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function renderMarkdown({ id }: { id: string }) { | ||||||
|  |   const postData = await rendererMarkdown(id); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     props: { | ||||||
|  |       postData, | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
| } | } | ||||||
| @ -5,6 +5,7 @@ import crypto from "crypto"; | |||||||
| import { db } from "~/server/db"; | import { db } from "~/server/db"; | ||||||
| import { auth } from "~/server/auth"; | import { auth } from "~/server/auth"; | ||||||
| import { minioClient, ensureBucketExists } from "~/utils/minioClient"; | import { minioClient, ensureBucketExists } from "~/utils/minioClient"; | ||||||
|  | import { getFileType } from "~/utils/fileType"; | ||||||
| 
 | 
 | ||||||
| export const config = { | export const config = { | ||||||
|   api: { |   api: { | ||||||
| @ -52,7 +53,7 @@ export async function POST(req: Request) { | |||||||
|               url: `/share?id=${fileId}`, |               url: `/share?id=${fileId}`, | ||||||
|               name: fileName, |               name: fileName, | ||||||
|               size: fileBuffer.length, |               size: fileBuffer.length, | ||||||
|               extension: info.mimeType, |               extension: getFileType(fileName), | ||||||
|               uploadedById: session.user.id, |               uploadedById: session.user.id, | ||||||
|             }, |             }, | ||||||
|           }); |           }); | ||||||
|  | |||||||
| @ -91,7 +91,7 @@ export default function SearchFile() { | |||||||
|                 className="flex place-content-end w-xxs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" |                 className="flex place-content-end w-xxs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" | ||||||
|               > |               > | ||||||
|                 <div className="self-center max-w-100 sm:max-w-50"> |                 <div className="self-center max-w-100 sm:max-w-50"> | ||||||
|                   <FilePreview fileId={file.id} fileType={file.extension} /> |                   <FilePreview fileId={file.id} fileType={file.extension} share={false} /> | ||||||
|                 </div> |                 </div> | ||||||
| 
 | 
 | ||||||
|                 <button onClick={() => router.push(pageUrl + file.url)}> |                 <button onClick={() => router.push(pageUrl + file.url)}> | ||||||
|  | |||||||
| @ -129,7 +129,7 @@ export default async function FilePreviewContainer({ | |||||||
|         </h1> |         </h1> | ||||||
|         <div className="mt-6"> |         <div className="mt-6"> | ||||||
|           {fileDetails.type !== "unknown" && ( |           {fileDetails.type !== "unknown" && ( | ||||||
|             <FilePreview fileId={fileDetails.id} fileType={fileDetails.type} /> |             <FilePreview fileId={fileDetails.id} fileType={fileDetails.type} share={true} /> | ||||||
|           )} |           )} | ||||||
|         </div> |         </div> | ||||||
|         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> |         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								src/app/styles/custom.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/app/styles/custom.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | .markdown-body ul, | ||||||
|  | .markdown-body ol { | ||||||
|  |   list-style: initial; /* Ensures bullets or numbers are displayed */ | ||||||
|  |   margin-left: 1.5em; /* Adds proper indentation */ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .markdown-body li { | ||||||
|  |   margin-bottom: 0.5em; /* Adds spacing between list items */ | ||||||
|  | } | ||||||
							
								
								
									
										78
									
								
								src/components/MarkdownRenderer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/components/MarkdownRenderer.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | |||||||
|  | import { useEffect } from "react"; | ||||||
|  | import "github-markdown-css/github-markdown.css"; | ||||||
|  | 
 | ||||||
|  | interface MarkdownRendererProps { | ||||||
|  |   markdownContent: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function MarkdownRenderer({ markdownContent }: MarkdownRendererProps) { | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (markdownContent) { | ||||||
|  |       const markdownContainer = document.querySelector("#markdown-preview"); | ||||||
|  |       if (!markdownContainer) return; | ||||||
|  | 
 | ||||||
|  |       const codeBlocks = markdownContainer.querySelectorAll("code"); | ||||||
|  | 
 | ||||||
|  |       codeBlocks.forEach((block) => { | ||||||
|  |         // Check if the block is already wrapped
 | ||||||
|  |         if (block.parentElement?.classList.contains("code-wrapper")) return; | ||||||
|  | 
 | ||||||
|  |         // Check if the code block is multiline
 | ||||||
|  |         const isMultiline = block.textContent?.includes("\n"); | ||||||
|  |         if (!isMultiline) return; | ||||||
|  | 
 | ||||||
|  |         // Create a wrapper only if it doesn't already exist
 | ||||||
|  |         const wrapper = document.createElement("div"); | ||||||
|  |         wrapper.className = "code-wrapper"; // Add a class to identify the wrapper
 | ||||||
|  |         wrapper.style.display = "flex"; | ||||||
|  |         wrapper.style.alignItems = "flex-start"; | ||||||
|  |         wrapper.style.justifyContent = "space-between"; | ||||||
|  |         wrapper.style.gap = "8px"; | ||||||
|  |         wrapper.style.width = "100%"; | ||||||
|  |         wrapper.style.position = "relative"; | ||||||
|  | 
 | ||||||
|  |         const codeContainer = document.createElement("div"); | ||||||
|  |         codeContainer.style.flex = "1"; | ||||||
|  |         codeContainer.appendChild(block.cloneNode(true)); | ||||||
|  | 
 | ||||||
|  |         const button = document.createElement("button"); | ||||||
|  |         button.innerHTML = ` | ||||||
|  |           <img src="/icons/copy.svg" alt="Copy" class="h-4 w-4" style="filter: invert(1) sepia(1) saturate(5) hue-rotate(180deg);"/> | ||||||
|  |         `;
 | ||||||
|  |         button.className = | ||||||
|  |           "copy-button inline-flex items-center justify-center bg-gray-200 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition"; | ||||||
|  |         button.style.marginLeft = "8px"; | ||||||
|  |         button.style.width = "1.5rem"; | ||||||
|  |         button.style.height = "1.5rem"; | ||||||
|  | 
 | ||||||
|  |         button.addEventListener("click", () => { | ||||||
|  |           navigator.clipboard.writeText(block.textContent || "").then(() => { | ||||||
|  |             button.innerHTML = ` | ||||||
|  |               <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||||
|  |                 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | ||||||
|  |               </svg> | ||||||
|  |             `;
 | ||||||
|  |             setTimeout(() => { | ||||||
|  |               button.innerHTML = ` | ||||||
|  |                 <img src="/icons/copy.svg" alt="Copy" class="h-4 w-4" style="filter: invert(1) sepia(1) saturate(5) hue-rotate(180deg);"/> | ||||||
|  |               `;
 | ||||||
|  |             }, 2000); | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         wrapper.appendChild(codeContainer); | ||||||
|  |         wrapper.appendChild(button); | ||||||
|  | 
 | ||||||
|  |         // Replace the original block with the wrapper
 | ||||||
|  |         block.replaceWith(wrapper); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, [markdownContent]); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="markdown-body max-w-full p-4 pt-0 pb-0" id="markdown-preview"> | ||||||
|  |       <div dangerouslySetInnerHTML={{ __html: markdownContent }} /> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user