codenuk_frontend_mine/src/app/github/repo/RepoTree.tsx
2025-10-10 09:02:08 +05:30

77 lines
3.1 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { getRepositoryStructure, type RepoStructureEntry } from "@/lib/api/github"
import { ChevronDown, ChevronRight, FileText, Folder } from "lucide-react"
type Node = {
name: string
path: string
type: 'file' | 'directory'
}
export default function RepoTree({ repositoryId, rootPath = "", onSelectFile, onSelectDirectory }: { repositoryId: string, rootPath?: string, onSelectFile: (path: string) => void, onSelectDirectory?: (path: string) => void }) {
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
const [loading, setLoading] = useState<Record<string, boolean>>({})
const [children, setChildren] = useState<Record<string, Node[]>>({})
const toggle = async (nodePath: string) => {
const next = !expanded[nodePath]
setExpanded(prev => ({ ...prev, [nodePath]: next }))
if (next && !children[nodePath]) {
setLoading(prev => ({ ...prev, [nodePath]: true }))
try {
const res = await getRepositoryStructure(repositoryId, nodePath)
const list = (res?.structure || []) as RepoStructureEntry[]
const mapped: Node[] = list.map(e => ({
name: (e as any).name || (e as any).filename || (((e as any).path || (e as any).relative_path || '').split('/').slice(-1)[0]) || 'unknown',
path: (e as any).path || (e as any).relative_path || '',
type: (e as any).type === 'directory' || (e as any).type === 'dir' ? 'directory' : 'file'
}))
setChildren(prev => ({ ...prev, [nodePath]: mapped }))
onSelectDirectory && onSelectDirectory(nodePath)
} finally {
setLoading(prev => ({ ...prev, [nodePath]: false }))
}
}
}
// initial load for root
useEffect(() => {
toggle(rootPath)
onSelectDirectory && onSelectDirectory(rootPath)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [repositoryId, rootPath])
const renderNode = (node: Node) => {
const isDir = node.type === 'directory'
const isOpen = !!expanded[node.path]
return (
<div key={node.path} className="select-none">
<div className="flex items-center gap-1 py-1 px-1 hover:bg-white/5 rounded cursor-pointer"
onClick={() => isDir ? toggle(node.path) : onSelectFile(node.path)}>
{isDir ? (
isOpen ? <ChevronDown className="h-4 w-4 text-white/70"/> : <ChevronRight className="h-4 w-4 text-white/70"/>
) : (
<span className="w-4"/>
)}
{isDir ? <Folder className="h-4 w-4 mr-1"/> : <FileText className="h-4 w-4 mr-1"/>}
<span className="truncate text-sm text-white">{node.name}</span>
</div>
{isDir && isOpen && (
<div className="pl-5 border-l border-white/10 ml-2">
{loading[node.path] && <div className="text-xs text-white/60 py-1">Loading</div>}
{(children[node.path] || []).map(ch => renderNode(ch))}
</div>
)}
</div>
)
}
return (
<div className="text-white/90">
{(children[rootPath] || []).map(n => renderNode(n))}
</div>
)
}