diff --git a/vibn-frontend/components/project/gitea-file-viewer.tsx b/vibn-frontend/components/project/gitea-file-viewer.tsx index 693468a4..1bef09d7 100644 --- a/vibn-frontend/components/project/gitea-file-viewer.tsx +++ b/vibn-frontend/components/project/gitea-file-viewer.tsx @@ -1,14 +1,10 @@ "use client"; -/** - * Read-only file viewer that pulls a file's content from Gitea via - * `GET /api/projects/[projectId]/file?path=…`. No syntax highlight - * yet — just monospaced text. Phase 2 can swap in Shiki/Prism if - * the founders ever read enough code here to need it. - */ - import { useEffect, useState } from "react"; -import { Loader2, AlertCircle, FileText } from "lucide-react"; +import { Loader2, AlertCircle, FileText, Copy, Check } from "lucide-react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { THEME } from "@/components/project/dashboard-ui"; interface GiteaFileViewerProps { projectId: string; @@ -29,6 +25,15 @@ export function GiteaFileViewer({ projectId, path }: GiteaFileViewerProps) { const [content, setContent] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [copied, setCopied] = useState(false); + + const copyCode = () => { + if (content) { + navigator.clipboard.writeText(content); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; useEffect(() => { if (!path) { @@ -46,16 +51,16 @@ export function GiteaFileViewer({ projectId, path }: GiteaFileViewerProps) { fetch(`/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`, { credentials: "include", }) - .then(async r => { + .then(async (r) => { const data = (await r.json()) as ApiResponse; if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`); if (data.type !== "file") throw new Error("Not a file"); return data.content ?? ""; }) - .then(c => { + .then((c) => { if (!cancelled) setContent(c); }) - .catch(err => { + .catch((err) => { if (!cancelled) setError(err.message || "Failed to load file"); }) .finally(() => { @@ -70,7 +75,7 @@ export function GiteaFileViewer({ projectId, path }: GiteaFileViewerProps) { if (!path) { return ( - + Pick a file from the codebase to preview it here. ); @@ -79,7 +84,11 @@ export function GiteaFileViewer({ projectId, path }: GiteaFileViewerProps) { if (loading) { return ( - + Loading {basename(path)}… ); @@ -88,17 +97,105 @@ export function GiteaFileViewer({ projectId, path }: GiteaFileViewerProps) { if (error) { return ( - - {error} + + {error} ); } + const extension = path.split(".").pop() || "text"; + const languageMap: Record = { + js: "javascript", + jsx: "jsx", + ts: "typescript", + tsx: "tsx", + json: "json", + html: "html", + css: "css", + scss: "scss", + md: "markdown", + sh: "bash", + yml: "yaml", + yaml: "yaml", + sql: "sql", + py: "python", + rs: "rust", + go: "go", + }; + const language = languageMap[extension] || "text"; + return ( -
-
-        {content}
-      
+
+
+
+ {basename(path)} +
+ +
+
+ + {content || ""} + +
); } @@ -109,36 +206,29 @@ function basename(p: string) { function Centered({ children }: { children: React.ReactNode }) { return ( -
+
{children}
); } -const INK = { - ink: "#1a1a1a", - mid: "#5f5e5a", - muted: "#a09a90", - border: "#e8e4dc", -} as const; - const wrap: React.CSSProperties = { flex: 1, minHeight: 0, overflow: "auto", - margin: "-4px -10px", -}; - -const pre: React.CSSProperties = { - margin: 0, - padding: "8px 10px", - fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", - fontSize: "0.78rem", - lineHeight: 1.55, - color: INK.ink, - whiteSpace: "pre", - tabSize: 2, + background: "#1e1e1e", + borderBottomLeftRadius: THEME.radiusSm, + borderBottomRightRadius: THEME.radiusSm, }; diff --git a/vibn-frontend/package.json b/vibn-frontend/package.json index 06f43573..cb21540e 100644 --- a/vibn-frontend/package.json +++ b/vibn-frontend/package.json @@ -70,6 +70,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^16.1.1", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "ssh2": "^1.17.0", @@ -87,6 +88,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13", "@types/ssh2": "^1.15.5", "eslint": "^9", "eslint-config-next": "16.0.1", diff --git a/vibn-frontend/pnpm-lock.yaml b/vibn-frontend/pnpm-lock.yaml index 32f0e716..3dfdd842 100644 --- a/vibn-frontend/pnpm-lock.yaml +++ b/vibn-frontend/pnpm-lock.yaml @@ -143,6 +143,9 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.16)(react@19.2.7) + react-syntax-highlighter: + specifier: ^16.1.1 + version: 16.1.1(react@19.2.7) remark-gfm: specifier: ^4.0.1 version: 4.0.1 @@ -189,6 +192,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.2.16) + '@types/react-syntax-highlighter': + specifier: ^15.5.13 + version: 15.5.13 '@types/ssh2': specifier: ^1.15.5 version: 1.15.5 @@ -2794,6 +2800,9 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/prismjs@1.26.6': + resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} + '@types/qs@6.15.1': resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} @@ -2805,6 +2814,9 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-syntax-highlighter@15.5.13': + resolution: {integrity: sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==} + '@types/react@19.2.16': resolution: {integrity: sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==} @@ -4080,6 +4092,9 @@ packages: fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + faye-websocket@0.11.4: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} @@ -4173,6 +4188,10 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -4364,18 +4383,30 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + highlightjs-vue@1.0.0: + resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + hono@4.12.23: resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} engines: {node: '>=16.9.0'} @@ -4907,6 +4938,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -5827,6 +5861,10 @@ packages: engines: {node: '>=16.13'} hasBin: true + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -5939,6 +5977,12 @@ packages: '@types/react': optional: true + react-syntax-highlighter@16.1.1: + resolution: {integrity: sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==} + engines: {node: '>= 16.20.2'} + peerDependencies: + react: '>= 0.14.0' + react-textarea-autosize@8.5.9: resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} engines: {node: '>=10'} @@ -5961,6 +6005,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + refractor@5.0.0: + resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -9603,6 +9650,8 @@ snapshots: pg-protocol: 1.14.0 pg-types: 2.2.0 + '@types/prismjs@1.26.6': {} + '@types/qs@6.15.1': {} '@types/range-parser@1.2.7': {} @@ -9611,6 +9660,10 @@ snapshots: dependencies: '@types/react': 19.2.16 + '@types/react-syntax-highlighter@15.5.13': + dependencies: + '@types/react': 19.2.16 + '@types/react@19.2.16': dependencies: csstype: 3.2.3 @@ -11169,6 +11222,10 @@ snapshots: dependencies: reusify: 1.1.0 + fault@1.0.4: + dependencies: + format: 0.2.2 + faye-websocket@0.11.4: dependencies: websocket-driver: 0.7.4 @@ -11315,6 +11372,8 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + format@0.2.2: {} + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -11548,6 +11607,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.9 @@ -11572,12 +11635,24 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 + highlight.js@10.7.3: {} + + highlightjs-vue@1.0.0: {} + hono@4.12.23: {} html-entities@2.6.0: {} @@ -12080,6 +12155,11 @@ snapshots: dependencies: js-tokens: 4.0.0 + lowlight@1.20.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + lru-cache@10.4.3: {} lru-cache@11.5.1: {} @@ -13521,6 +13601,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + prismjs@1.30.0: {} + progress@2.0.3: {} prop-types@15.8.1: @@ -13701,6 +13783,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 + react-syntax-highlighter@16.1.1(react@19.2.7): + dependencies: + '@babel/runtime': 7.29.7 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.30.0 + react: 19.2.7 + refractor: 5.0.0 + react-textarea-autosize@8.5.9(@types/react@19.2.16)(react@19.2.7): dependencies: '@babel/runtime': 7.29.7 @@ -13731,6 +13823,13 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + refractor@5.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/prismjs': 1.26.6 + hastscript: 9.0.1 + parse-entities: 4.0.2 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.9